当前位置: 首页 > news >正文

Linux系统:线程的互斥和安全

文章目录

  • 前言
  • 一、线程竞争案例
  • 二、互斥,锁
  • 三,线程安全
    • 3-1 为什么会线程不安全
  • 四,重入函数
    • 4-1 两种重入场景的具体分析
  • 五,死锁
    • 5-1 死锁的核心概念
    • 5-2 死锁产生的四大必要条件
    • 5-3 死锁的危害
    • 5-4 避免死锁的常用方法


前言

学线程互斥的用处在于:在多线程程序里,很多数据和资源是共享的(比如全局变量、文件、socket、内存池)。如果不加限制,多个线程可能会在同一时间修改同一份数据,导致结果错误或者程序崩溃。互斥锁的作用就是保证某一段代码在同一时刻只能被一个线程执行,从而避免数据竞争,让结果稳定可靠。


一、线程竞争案例

我们来编写一个线程竞争资源的代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
using namespace std;class customer {
public:int _ticket_num = 0;   // 该顾客买到的票数pthread_t _tid;        // 线程IDstring _name;          // 顾客名字
};int g_ticket = 10000;      // 总票数void* buyTicket(void* args) {customer* cust = (customer*)args;while (true) {if (g_ticket > 0) {usleep(1000);  // 模拟出票耗时cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;cust->_ticket_num++;} else {break;}}return nullptr;
}int main() {vector<customer> custs(5);// 创建 5 个顾客线程for (int i = 0; i < 5; i++) {custs[i]._name = "customer-" + to_string(i + 1);pthread_create(&custs[i]._tid, nullptr, buyTicket, &custs[i]);}// 等待所有顾客线程结束for (int i = 0; i < 5; i++) {pthread_join(custs[i]._tid, nullptr);}// 打印每个顾客买到的票数for (int i = 0; i < 5; i++) {cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;}return 0;
}

我们先从自定义函数和自定义类开始讲解,再到main函数的讲解:

class customer:有三个参数,都是顾客自身的信息,在整体程序当中,每一个顾客对应一个线程

void* buyTicket:用于给顾客售票,顾客每增加一张票,则g_ticket减1

main:创建五个进程,并且相继竞争购票

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day6$ ./exe
......
customer-3 get ticket: 0
customer-4 get ticket: -1
customer-1 get ticket: -2
customer-5 get ticket: -3
customer-1 get tickets: 2000
customer-2 get tickets: 2001
customer-3 get tickets: 2000
customer-4 get tickets: 2001
customer-5 get tickets: 2002

那么这里就有问题了,明明只有1000张票,为什么五个人加起来的数量是1004张呢

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • --ticket 操作本身就不是一个原子操作

这个ticket在线程当中属于共享资源,因为只有一个CUP并且它是单核的,一次只能执行一个进程,为了实现进程的同步,操作系统内核会在线程还没执行完函数时打断线程,让CUP运行其它的线程,但是上一个线程还没执行玩ticket,下一个进程就已经进入if函数了,所以会导致--ticket被运行多次

  • g_ticket 是共享资源,因为所有线程都可以访问和修改它。多个线程同时访问它,就有可能出现冲突。

  • 虽然单核 CPU 在 同一时刻 只能执行一个线程,但操作系统会通过 时间片轮转 或 线程调度 不断切换线程。

    • 一个线程可能执行到一半(比如刚判断 g_ticket > 0)就被操作系统挂起。
    • CPU 会切换给另一个线程去执行。

假设线程 A 执行到:

if (g_ticket > 0) // 假设 g_ticket = 1

此时线程 A 还没来得及执行 g_ticket--。操作系统把 CPU 切给线程 B。线程 B 也执行到同样的 if (g_ticket > 0) 判断,此时它看到 g_ticket 还是 1,于是也进入 if。最终,两个线程都执行了 g_ticket--,但是 g_ticket 只应该被减一次。这就是 竞态条件:多个线程同时操作共享资源,导致结果错误


二、互斥,锁

要解决上述共享资源冲突问题,需要满足三点条件:

  • 互斥执行:当某个线程进入临界区执行代码时,其他线程不能同时进入该临界区。
  • 公平进入:如果多个线程同时请求进入临界区,而此时没有线程在执行,只允许其中一个线程进入。
  • 非阻塞退出:线程在临界区外时,不得阻止其他线程进入临界区

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
在这里插入图片描述


演示代码:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>
using namespace std;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
class customer
{
public:int _ticket_num = 0;pthread_t _tid;string _name;
};int g_ticket = 10000;void* buyTicket(void* args)
{customer* cust = (customer*)args;while(true){pthread_mutex_lock(&mutex);if(g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;cust->_ticket_num++;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}return nullptr;
}int main()
{vector<customer> custs(5);for(int i = 0; i < 5; i++){custs[i]._name= "customer-" + to_string(i + 1);pthread_create(&custs[i]._tid, nullptr, buyTicket, &custs[i]);}for(int i = 0; i < 5; i++){pthread_join(custs[i]._tid, nullptr);}for(int i = 0; i < 5; i++){cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;}return 0;
}

在这段代码里:

  • pthread_mutex_lock(&mutex) 让线程尝试获取锁,如果别的线程已经持有锁,它就会阻塞等待。
  • 当线程获得锁后,它才能进入临界区操作共享资源 g_ticket
  • 执行完之后,调用 pthread_mutex_unlock(&mutex) 释放锁,这样别的线程才能继续进入临界区。

所以,锁的作用就是:

  • 保证同一时刻只有一个线程能修改 g_ticket,避免出现多个线程同时减票而导致的数据错误。

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day6$ ./exe
......
customer-1 get tickets: 2214
customer-2 get tickets: 2391
customer-3 get tickets: 1761
customer-4 get tickets: 1806
customer-5 get tickets: 1828

上述是全局静态锁,如果是局部锁需要在使用完之后用pthread_mutex_destroy进行销毁
互斥锁初始化方式主要有三种

  • 静态初始化(全局/静态作用域)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 在 编译期 就完成初始化。

  • 适合全局变量、静态变量。

  • 生命周期随进程结束自动回收,不需要显式销毁

  • 动态初始化

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_destroy(&mutex);
  • 在 运行时 用 pthread_mutex_init 初始化。
  • 适合函数内的局部变量,或需要 动态分配的结构体里的成员。
  • 用完必须 pthread_mutex_destroy,否则可能导致资源泄漏。

三,线程安全

线程安全指的是:当多个线程同时访问某个函数、数据结构或代码片段时,程序的行为依然是正确的、可预期的,不会出现数据错乱或未定义行为

换句话说:

  • 线程安全:多个线程并发访问 → 程序结果仍然正确。
  • 线程不安全:多个线程并发访问 → 可能导致错误(数据竞争、死锁、崩溃)。

3-1 为什么会线程不安全

根本原因是:多线程共享内存,但调度是抢占式的。
假设我们有一个全局变量:

int counter = 0;void *worker(void *arg) {for (int i = 0; i < 10000; i++) {counter++;  // 不是原子操作!}return NULL;
}

如果两个线程同时执行 counter++:

实际会分解成三步:读 countercounter + 1写回 counter
假如两个线程交叉执行,就可能丢失更新(最终结果小于 20000)。
这就是 数据竞争,典型的线程不安全,所以我们在这种线程不安全的情况,我们可以使用锁来实现线程安全。
线程安全 = 在多线程并发情况下,程序逻辑和数据结果依旧正确,不会出现竞态问题。


四,重入函数

重入的核心是同一个函数被多个执行流交错执行。想象一个函数正在执行到一半,突然被打断(可能是另一个线程开始执行,或者信号处理函数触发),而这个打断它的执行流也调用了同一个函数,这就发生了重入


4-1 两种重入场景的具体分析

  1. 多线程重入(并发重入)
    当多个线程同时调用同一个函数时,就可能发生重入

可重入示例

// 可重入函数:只使用局部变量
int add(int a, int b) {int temp;  // 局部变量,每个线程有独立副本temp = a + b;return temp;
}

每个线程调用add()时,局部变量temp是线程私有,不会互相干扰。

不可重入示例

// 不可重入函数:使用全局变量
int global_num = 0;
int increment() {global_num++;  // 读取-修改-写入三步操作,可能被打断return global_num;
}

  1. 信号导致的重入(异步重入)
    当程序正在执行函数 A 时,突然收到信号,系统会暂停当前执行流,转去执行信号处理函数。如果信号处理函数也调用了函数 A,就会发生重入。

危险示例

#include <signal.h>
#include <stdio.h>FILE *file;void signal_handler(int signum) {// 信号处理函数也操作file,导致重入fputs("Signal handled\n", file);
}int main() {file = fopen("test.txt", "w");signal(SIGINT, signal_handler);  // 注册Ctrl+C的处理函数// 主程序正在操作file时,若收到信号会导致重入fputs("Main writing\n", file);// ... 其他操作fclose(file);return 0;
}
  • 主程序正在执行fputs()(操作全局变量file)时,若按下 Ctrl+C 触发信号
  • 信号处理函数也调用fputs()操作同一个file
  • 可能导致文件缓冲区数据混乱,甚至程序崩溃

3、可重入函数的判定准则
一个函数要成为可重入函数,必须满足:

  • 不使用全局变量或静态变量,或对其访问进行特殊保护
  • 不使用 malloc/free(会操作全局内存管理结构)
  • 不调用其他不可重入函数(如标准库中的printffputsI/O函数)
  • 不依赖硬件资源的状态(如不直接操作硬件寄存器)

五,死锁

死锁是并发编程中一种常见且危险的状态,指两个或多个执行流(线程、进程)相互等待对方持有的资源,且彼此都无法继续推进的僵局


5-1 死锁的核心概念

当多个执行执行流同时竞争有限的共享资源时,若每个执行流都持有一部分资源,同时又等待其他执行流释放所需资源,就会形成循环等待,导致所有执行流都无法继续执行,这种状态称为死锁。


5-2 死锁产生的四大必要条件

  • 互斥条件:资源具有排他性,同一时间只能被一个执行流使用(如一把锁只能被一个线程持有)。
  • 持有并等待条件:执行流已经持有至少一个资源,同时又在等待获取其他执行流持有的资源。
  • 不可剥夺条件:已获取的资源不能被强制剥夺,只能由持有者主动释放(如线程持有的锁不能被其他线程强制释放)。
  • 循环等待条件:存在执行流的循环链,每个执行流都在等待下一个执行流持有的资源(如线程 A 等线程 B 的资源,线程 B 等线程 A 的资源)。

演示代码:

#include <pthread.h>
#include <stdio.h>// 定义两个全局锁(资源)
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;// 线程1的执行函数:先锁lock1,再等lock2
void *thread1(void *arg) {pthread_mutex_lock(&lock1);printf("线程1持有lock1,等待lock2...\n");// 模拟处理时间,增加死锁概率sleep(1);pthread_mutex_lock(&lock2);  // 等待线程2释放lock2// 业务操作(实际中不会执行到)pthread_mutex_unlock(&lock2);pthread_mutex_unlock(&lock1);return NULL;
}// 线程2的执行函数:先锁lock2,再等lock1
void *thread2(void *arg) {pthread_mutex_lock(&lock2);printf("线程2持有lock2,等待lock1...\n");// 模拟处理时间,增加死锁概率sleep(1);pthread_mutex_lock(&lock1);  // 等待线程1释放lock1// 业务操作(实际中不会执行到)pthread_mutex_unlock(&lock1);pthread_mutex_unlock(&lock2);return NULL;
}int main() {pthread_t t1, t2;pthread_create(&t1, NULL, thread1, NULL);pthread_create(&t2, NULL, thread2, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);return 0;
}

执行结果:

线程 1 持有 lock1 并等待 lock2线程 2 持有 lock2 并等待 lock1,形成循环等待,程序永远卡在等待状态,即死锁。


5-3 死锁的危害

  • 程序卡住,无法继续执行,需要强制终止
  • 资源被永久占用,无法释放
  • 难以调试,死锁可能在特定 timing 下才触发,复现困难

5-4 避免死锁的常用方法

  • 破坏循环等待条件:对所有资源按固定顺序获取(如规定必须先获取 lock1,再获取 lock2)。
    破坏持有并等待条件:一次性获取所有所需资源,获取不到则释放已持有的资源并重试。
    使用带超时的锁:如pthread_mutex_timedlock,超时后释放资源并重新尝试。
    定期检测死锁:通过工具(如pstackgdb)或自定义算法检测死锁,发现后强制释放资源。

文章转载自:

http://bW5cLJrt.cfnsn.cn
http://rNOOni98.cfnsn.cn
http://JIZOipJS.cfnsn.cn
http://OLGfcFi3.cfnsn.cn
http://eGqY7FrB.cfnsn.cn
http://AcHDqJSD.cfnsn.cn
http://q76bEveJ.cfnsn.cn
http://fc9wOaEV.cfnsn.cn
http://3xv2lAhO.cfnsn.cn
http://dBGHJRTQ.cfnsn.cn
http://mgn6ULNe.cfnsn.cn
http://UU7N9Tvl.cfnsn.cn
http://DN9LJWJS.cfnsn.cn
http://8nGnGqXT.cfnsn.cn
http://d4Nmcb17.cfnsn.cn
http://EhCtAoxW.cfnsn.cn
http://eZ5izy8h.cfnsn.cn
http://2CvoKYwy.cfnsn.cn
http://ix3qakkP.cfnsn.cn
http://cAQ2hcbF.cfnsn.cn
http://Fk3dFg9R.cfnsn.cn
http://v5UAcKTG.cfnsn.cn
http://84wlblu5.cfnsn.cn
http://fqNG1fk4.cfnsn.cn
http://SrrgJAUK.cfnsn.cn
http://IWwcYHfc.cfnsn.cn
http://yktpEJif.cfnsn.cn
http://EgIF3rbK.cfnsn.cn
http://5npJjDh9.cfnsn.cn
http://qWj7UXbH.cfnsn.cn
http://www.dtcms.com/a/373349.html

相关文章:

  • # 集成学习完整指南:从理论到实践
  • CSS rem单位
  • 云原生与 AI 加持下,DevOps平台的演进趋势、选型建议与推荐指南
  • 软件研发如何选对方法论?传统计划驱动与敏捷价值驱动的全面对比
  • CVE-2025-57052:cJSON库存在CVSS 9.8高危JSON解析漏洞(含PoC)
  • 基于大数据的二手交易推荐系统设计与实现(代码+数据库+LW)
  • 9.8 ajax+php基础语法
  • USB系统学习笔记 - 从概念到抓包解析
  • 前端框架对比分析:离线PWA + Cloudflare Workers部署
  • TensorFlow深度学习实战(37)——深度学习的数学原理
  • iOS混淆工具实战,健身与健康监测类 App 的隐私与算法保护
  • ChatAI项目-ChatGPT-SDK组件工程
  • 关于对逾期提醒的定时任务~改进完善
  • BKY(莱德因):基于线粒体靶向的细胞级御龄科学实践
  • 学习日记-SpringMVC-day50-9.8
  • VUE3加载cesium,导入czml的星座后页面卡死BUG 修复
  • Redis集群——redis cluster(去中心化)
  • HCIE安全为什么是T0级别的选项?
  • IDEA开启并配置Services窗口(一个项目开启多个项目运行窗口并且显示端口)
  • Sourcetree使用
  • 【Docker】Docker安装
  • 个人日记系统00
  • 20.42 QLoRA微调实战:四层提示工程让批量数据生成错误率跌破0.5%
  • S32K3平台eMIOS 应用说明
  • iOS 开发入门指南-HelloWorld
  • HCIE数通/云计算真机实验机架展示
  • 【.Net技术栈梳理】04-核心框架与运行时(线程处理)
  • 量化金融|基于算法和模型的预测研究综述
  • HarmonyOS 数据处理性能优化:算法 + 异步 + 分布式实战
  • 1304. 和为零的 N 个不同整数