【Linux】线程同步与互斥深度解析:从锁机制到生产者消费者模型
目录
一、引言:多线程共享资源的问题
1.1 为什么需要同步与互斥?
1.2 核心概念铺垫
二、线程互斥:用互斥量(mutex)守护临界资源
2.1 互斥的核心:临界资源与临界区
2.2 互斥量接口与实战
2.3 RAII风格锁封装:避免锁泄漏
三、线程同步:条件变量(cond)实现有序协作
3.1 同步的意义:解决“竞态条件”
3.2 条件变量接口与核心原理
3.3 条件变量使用规范:避免伪唤醒
3.4 条件变量封装:与互斥量解耦
四、生产者消费者模型:同步互斥的经典实战
4.1 模型核心:321原则
4.2 实现一:基于阻塞队列(动态大小)
4.3 实现二:基于环形队列(固定大小)
4.4 两种模型对比与选型
五、线程安全与可重入问题
5.1 线程安全:多线程访问的一致性保障
5.2 可重入函数:重入场景下的正确性
5.3 核心区别与联系
六、死锁
6.1 死锁的四个必要条件
6.2 避免死锁的实战方法
七、总结
一、引言:多线程共享资源的问题
多线程的核心优势是共享进程资源、提高并发效率,但共享资源带来了一个致命问题——竞态条件(Race Condition):多个线程并发操作共享资源时,由于执行时序不确定,导致最终结果与预期不符。比如多线程售票系统可能卖出负数票,银行转账可能导致余额不一致。
线程同步与互斥的核心目标,就是在保证多线程并发的同时,解决竞态条件,确保数据一致性。
1.1 为什么需要同步与互斥?
举个直观例子:未加保护的售票系统
#include<stdio.h>#include<pthread.h>#include<unistd.h>int ticket =100;// 共享资源void*sell_ticket(void* arg){char* id =(char*)arg;while(1){if(ticket >0){usleep(1000);// 模拟业务耗时printf("%s sells ticket:%d\n", id, ticket); ticket--;// 非原子操作}else{break;}}}intmain(){ pthread_t t1, t2, t3, t4;pthread_create(&t1,NULL, sell_ticket,(void*)"thread 1");pthread_create(&t2,NULL, sell_ticket,(void*)"thread 2");pthread_create(&t3,NULL, sell_ticket,(void*)"thread 3");pthread_create(&t4,NULL, sell_ticket,(void*)"thread 4");pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_join(t3,NULL);pthread_join(t4,NULL);return0;}运行结果:出现负数票

原因:
if (ticket > 0)判断后,线程可能被切换,其他线程继续修改ticket;ticket--对应三条汇编指令(load→update→store),非原子操作,可能被打断。
解决思路:互斥保证同一时间只有一个线程访问临界资源,同步保证线程按合理顺序访问。
1.2 核心概念铺垫
- 共享资源:多个线程均可访问的资源(如全局变量、堆内存、文件描述符);
- 临界资源:需要被保护的共享资源(如售票系统的
ticket); - 临界区:访问临界资源的代码段(如
if (ticket > 0)到ticket--的代码); - 互斥:任何时刻仅允许一个线程进入临界区,保证资源独占访问;
- 同步:在互斥基础上,保证线程按预期顺序访问资源(如生产者先生产,消费者后消费);
- 原子操作:不可被打断的操作(如硬件提供的
swap指令),是互斥的底层基础。
二、线程互斥:用互斥量(mutex)守护临界资源
互斥量(mutex)是Linux提供的核心互斥工具,本质是一把“锁”,通过原子操作实现锁的获取与释放,确保临界区的独占访问。
2.1 互斥的核心:临界资源与临界区
- 临界区必须是“最小粒度”:仅包含访问临界资源的必要代码,避免过度阻塞影响并发效率;
- 互斥的三大要求:
- 互斥性:同一时间仅一个线程进入临界区;
- 空闲让进:临界区空闲时,允许等待线程进入;
- 有限等待:线程等待锁的时间有限,避免饥饿。
2.2 互斥量接口与实战
POSIX线程库提供互斥量相关接口,需包含<pthread.h>,链接时加-lpthread。
2.2.1 互斥量核心接口
| 接口 | 功能 | 关键说明 |
|---|---|---|
pthread_mutex_init | 动态初始化互斥量 | attr=NULL使用默认属性 |
PTHREAD_MUTEX_INITIALIZER | 静态初始化互斥量 | 全局或静态互斥量使用,无需销毁 |
pthread_mutex_lock | 加锁 | 阻塞等待,直到获取锁 |
pthread_mutex_unlock | 解锁 | 必须由加锁线程解锁 |
pthread_mutex_destroy | 销毁互斥量 | 静态初始化的互斥量无需销毁 |
2.2.2 实战:改进售票系统
#include<stdio.h>#include<pthread.h>#include<unistd.h>int ticket =100; pthread_mutex_t mutex;void*sell_ticket(void* arg){char* id =(char*)arg;while(1){int current_ticket =0;// 临界区:只保护共享数据的读写pthread_mutex_lock(&mutex);if(ticket <=0){pthread_mutex_unlock(&mutex);break;} current_ticket = ticket; ticket--;pthread_mutex_unlock(&mutex);// 模拟耗时操作usleep(1000);printf("%s sells ticket:%d\n", id, current_ticket);}returnNULL;}intmain(){ pthread_t t1, t2, t3, t4;pthread_mutex_init(&mutex,NULL);pthread_create(&t1,NULL, sell_ticket,(void*)"thread 1");pthread_create(&t2,NULL, sell_ticket,(void*)"thread 2");pthread_create(&t3,NULL, sell_ticket,(void*)"thread 3");pthread_create(&t4,NULL, sell_ticket,(void*)"thread 4");pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_join(t3,NULL);pthread_join(t4,NULL);pthread_mutex_destroy(&mutex);return0;}运行结果:无负数票,每个线程独占临界区,数据一致。

2.3 RAII风格锁封装:避免锁泄漏
手动加锁/解锁容易出现“忘记解锁”或“异常时未解锁”的问题,采用RAII(资源获取即初始化)风格封装锁,利用C++析构函数自动释放锁:
// Lock.hpp#pragmaonce#include<pthread.h>namespace LockModule {classMutex{public:Mutex(){pthread_mutex_init(&_mutex,NULL);}~Mutex(){pthread_mutex_destroy(&_mutex);}voidLock(){pthread_mutex_lock(&_mutex);}voidUnlock(){pthread_mutex_unlock(&_mutex);} pthread_mutex_t*GetRawMutex(){return&_mutex;}private:Mutex(const Mutex&)=delete;// 禁止拷贝 Mutex&operator=(const Mutex&)=delete; pthread_mutex_t _mutex;};// RAII锁,构造加锁,析构解锁classLockGuard{public:LockGuard(Mutex& mutex):_mutex(mutex){ _mutex.Lock();}~LockGuard(){ _mutex.Unlock();}private:LockGuard(const LockGuard&)=delete; LockGuard&operator=(const LockGuard&)=delete; Mutex& _mutex;};}使用改进:
// test.cpp#include"Lock.hpp"#include<stdio.h>#include<unistd.h>usingnamespace LockModule;int ticket =100; Mutex mutex;void*sell_ticket(void* arg){char* id =(char*)arg;while(1){// 先获取数据,尽快释放锁int current_ticket =0;{ LockGuard lock(mutex);if(ticket <=0)break; current_ticket = ticket; ticket--;}// 锁在这里就释放了// 模拟耗时操作usleep(1000);printf("%s sells ticket:%d\n", id, current_ticket);}}intmain(){ pthread_t t1, t2, t3, t4;pthread_create(&t1,NULL, sell_ticket,(void*)"thread 1");pthread_create(&t2,NULL, sell_ticket,(void*)"thread 2");pthread_create(&t3,NULL, sell_ticket,(void*)"thread 3");pthread_create(&t4,NULL, sell_ticket,(void*)"thread 4");pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_join(t3,NULL);pthread_join(t4,NULL);return0;}
三、线程同步:条件变量(cond)实现有序协作
互斥量解决了“同一时间只有一个线程访问资源”,但无法解决“线程按顺序访问”的问题。例如上面的售票系统输出顺序我们是无法控制的,此时需要条件变量。
3.1 同步的意义:解决“竞态条件”
- 竞态条件:因线程执行时序不确定导致程序异常(如消费者先消费空队列);
- 同步:在保证互斥的基础上,让线程按预期顺序执行,避免竞态条件。
3.2 条件变量接口与核心原理
条件变量允许线程在某个条件不满足时主动等待,当条件满足时被其他线程唤醒。它本身不保证互斥,需与互斥量配合使用,核心接口如下:
| 接口 | 功能 | 关键说明 |
|---|---|---|
pthread_cond_init | 动态初始化条件变量 | attr=NULL使用默认属性 |
PTHREAD_COND_INITIALIZER | 静态初始化条件变量 | 全局或静态条件变量使用 |
pthread_cond_wait | 等待条件满足 | 1. 自动释放互斥量;2. 被唤醒后重新获取互斥量 |
pthread_cond_signal | 唤醒一个等待线程 | 唤醒等待队列中的一个线程 |
pthread_cond_broadcast | 唤醒所有等待线程 | 避免线程饥饿 |
pthread_cond_destroy | 销毁条件变量 | 静态初始化的无需销毁 |
为什么pthread_cond_wait需要互斥量?
- 条件的判断和修改依赖共享资源(如队列是否为空),需互斥保护;
- 解锁和等待必须是原子操作:若先解锁再等待,可能错过其他线程的信号,导致永久阻塞。
pthread_cond_wait内部自动完成“解锁→等待→唤醒后加锁”的原子流程。
3.3 条件变量使用规范
等待条件:必须用while循环,而非if,避免伪唤醒(线程被唤醒但条件未满足):
pthread_mutex_lock(&mutex);while(条件不满足){// 如队列空、队列满pthread_cond_wait(&cond,&mutex);}// 处理临界资源pthread_mutex_unlock(&mutex);发送信号:修改条件后发送信号,且需在互斥量保护内:
pthread_mutex_lock(&mutex); 修改条件;// 如向队列添加元素、移除元素pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);3.4 条件变量封装
// Cond.hpp#pragmaonce#include"Lock.hpp"#include<pthread.h>namespace CondModule {usingnamespace LockModule;classCond{public:Cond(){pthread_cond_init(&_cond,NULL);}~Cond(){pthread_cond_destroy(&_cond);}voidWait(Mutex& mutex){// 传入互斥量,解耦pthread_cond_wait(&_cond, mutex.GetRawMutex());}voidSignal(){pthread_cond_signal(&_cond);}voidBroadcast(){pthread_cond_broadcast(&_cond);}private:Cond(const Cond&)=delete; Cond&operator=(const Cond&)=delete; pthread_cond_t _cond;};}// test.cpp#include"Cond.hpp"#include<stdio.h>#include<unistd.h>usingnamespace LockModule;usingnamespace CondModule;int ticket =100; Mutex mutex; Cond cond;// 当前应该执行的线程ID(1,2,3,4)int current_thread_id =1;void*sell_ticket(void* arg){int my_id =*(int*)arg;char id_str[20];sprintf(id_str,"thread %d", my_id);while(1){{ LockGuard lock(mutex);// 等待轮到自己执行while(current_thread_id != my_id && ticket >0){ cond.Wait(mutex);}// 检查票是否卖完if(ticket <=0){// 通知下一个线程,让它也退出 current_thread_id =(current_thread_id %4)+1; cond.Broadcast();// 唤醒所有等待的线程break;}// 卖票int current_ticket = ticket; ticket--;// 更新下一个应该执行的线程 current_thread_id =(current_thread_id %4)+1;// 打印售票信息(在锁内打印确保顺序)printf("%s sells ticket:%d\n", id_str, current_ticket);// 通知下一个线程 cond.Broadcast();// 唤醒所有等待的线程}// 在锁外模拟耗时操作usleep(1000);}returnNULL;}intmain(){ pthread_t t1, t2, t3, t4;int thread_ids[4]={1,2,3,4};pthread_create(&t1,NULL, sell_ticket,(void*)&thread_ids[0]);pthread_create(&t2,NULL, sell_ticket,(void*)&thread_ids[1]);pthread_create(&t3,NULL, sell_ticket,(void*)&thread_ids[2]);pthread_create(&t4,NULL, sell_ticket,(void*)&thread_ids[3]);pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_join(t3,NULL);pthread_join(t4,NULL);return0;}这样我们就利用了条件变量实现了售票的同步。

四、生产者消费者模型:同步互斥的经典实战
生产者消费者模型是同步互斥的典型应用,核心是“解耦生产者和消费者,平衡处理能力”,遵循321原则:
- 3种关系:生产者与生产者(互斥)、消费者与消费者(互斥)、生产者与消费者(同步+互斥);
- 2个角色:生产者(生产数据)、消费者(消费数据);
- 1个容器:阻塞队列/环形队列(存储数据,解耦角色)。
4.1 模型核心价值
- 解耦:生产者和消费者无需知道对方存在,仅通过容器交互;
- 支持并发:多个生产者/消费者可同时工作;
- 平衡负载:生产者生产快时,容器缓存数据;消费者消费快时,容器空则阻塞等待。
4.2 实现一:基于阻塞队列(动态大小)
阻塞队列是动态大小的容器,特点:
- 队列为空时,消费者阻塞;
- 队列满时(可选限制),生产者阻塞;
基于std::queue+互斥量+条件变量实现。
完整代码(BlockQueue.hpp)
#pragmaonce#include<queue>#include"Lock.hpp"#include"Cond.hpp"namespace Model {usingnamespace LockModule;usingnamespace CondModule;template<typenameT>classBlockQueue{public:BlockQueue(int cap =10):_cap(cap),_stop(false){}~BlockQueue(){// 析构时停止队列,唤醒所有阻塞线程Stop();}// 停止队列voidStop(){ LockGuard lock(_mutex); _stop =true;// 广播所有条件变量,唤醒所有阻塞线程 _prod_cond.Broadcast(); _cons_cond.Broadcast();}// 生产者入队:返回是否成功(如果队列已停止,返回 false)boolEnqueue(const T& data){ LockGuard lock(_mutex);// 队列满且未停止时,阻塞等待while(_queue.size()>= _cap &&!_stop){ _prod_cond.Wait(_mutex);}// 如果队列已停止,直接返回失败if(_stop){returnfalse;}// 生产数据 _queue.push(data);// 唤醒一个等待的消费者(队列非空了) _cons_cond.Signal();returntrue;}// 消费者出队:返回是否成功(如果队列已停止且为空,返回 false)boolDequeue(T& data){ LockGuard lock(_mutex);// 队列为空且未停止时,阻塞等待while(_queue.empty()&&!_stop){ _cons_cond.Wait(_mutex);}// 如果队列已停止且为空,返回失败if(_stop && _queue.empty()){returnfalse;}// 消费数据 data = _queue.front(); _queue.pop();// 唤醒一个等待的生产者(队列未满了) _prod_cond.Signal();returntrue;}// 判断队列是否已停止(可选接口)boolIsStopped()const{ LockGuard lock(_mutex);return _stop;} size_t Size(){ LockGuard lock(_mutex);return _queue.size();}private: std::queue<T> _queue;// 任务队列int _cap;// 队列最大容量 Mutex _mutex;// 保护队列的互斥量 Cond _prod_cond;// 生产者条件变量(队列不满) Cond _cons_cond;// 消费者条件变量(队列不空)bool _stop;// 退出标志(是否停止队列)};}测试代码
#include"BlockQueue.hpp"// 包含修复后的阻塞队列实现#include<pthread.h>#include<iostream>#include<unistd.h>#include<string>usingnamespace Model;usingnamespace std;// 全局阻塞队列(容量设为 3,方便观察阻塞效果) BlockQueue<string>g_task_queue(3);// 生产者线程函数:生成 10 个任务,之后退出void*ProducerThread(void* arg){constchar* producer_name =static_cast<constchar*>(arg); cout <<"["<< producer_name <<"] 启动,开始生产任务..."<< endl;for(int i =0; i <10;++i){// 生成任务(模拟任务数据) string task ="任务-"+to_string(i);// 入队(队列满时会阻塞)bool ret = g_task_queue.Enqueue(task);if(!ret){ cout <<"["<< producer_name <<"] 队列已停止,停止生产"<< endl;break;} cout <<"["<< producer_name <<"] 生产任务:"<< task <<" | 队列当前大小:"<< g_task_queue.Size()<< endl;// 模拟生产耗时(1秒/个)sleep(1);} cout <<"["<< producer_name <<"] 生产完毕,退出线程"<< endl;returnnullptr;}// 消费者线程函数:持续消费任务,直到队列停止且为空void*ConsumerThread(void* arg){constchar* consumer_name =static_cast<constchar*>(arg); cout <<"["<< consumer_name <<"] 启动,开始消费任务..."<< endl;while(true){ string task;// 出队(队列为空时会阻塞)bool ret = g_task_queue.Dequeue(task);if(!ret){ cout <<"["<< consumer_name <<"] 队列已停止且无任务,退出消费"<< endl;break;} cout <<"["<< consumer_name <<"] 消费任务:"<< task <<" | 队列当前大小:"<< g_task_queue.Size()<< endl;// 模拟消费耗时(2秒/个,故意比生产慢,触发队列满阻塞生产者)sleep(2);} cout <<"["<< consumer_name <<"] 消费完毕,退出线程"<< endl;returnnullptr;}intmain(){ pthread_t producer, consumer;constchar* prod_name ="生产者";constchar* cons_name ="消费者";// 创建生产者和消费者线程pthread_create(&producer,nullptr, ProducerThread,(void*)prod_name);pthread_create(&consumer,nullptr, ConsumerThread,(void*)cons_name);// 主线程等待 20 秒(确保生产者完成所有任务,消费者消费完毕) cout <<"主线程:等待 20 秒后停止队列..."<< endl;sleep(20);// 停止队列(优雅唤醒阻塞的线程) cout <<"主线程:停止队列"<< endl; g_task_queue.Stop();// 等待线程退出(回收资源)pthread_join(producer,nullptr);pthread_join(consumer,nullptr); cout <<"主线程:所有线程退出,程序结束"<< endl;return0;}
4.3 实现二:基于环形队列(固定大小)
环形队列是固定大小的数组,用模运算实现“环形”,特点:
- 无需动态扩容,性能更优;
- 用信号量实现同步,互斥量实现生产者/消费者内部互斥;
- 适合对性能要求高、数据量可预估的场景。
核心思路
- 信号量
_empty:表示空闲空间数,初始为队列容量; - 信号量
_full:表示已用空间数,初始为0; - 生产者:
P(_empty)→生产→V(_full); - 消费者:
P(_full)→消费→V(_empty);
互斥量_prod_mutex/_cons_mutex:保证多个生产者/消费者的互斥访问。
完整代码(RingQueue.hpp)
#pragmaonce#include<vector>#include<pthread.h>#include<semaphore.h>namespace Model {template<typenameT>classRingQueue{public:RingQueue(int cap =10):_cap(cap),_prod_idx(0),_cons_idx(0){ _queue.resize(_cap);// 初始化信号量sem_init(&_empty,0, _cap);// 0表示线程间共享sem_init(&_full,0,0);// 初始化互斥量pthread_mutex_init(&_prod_mutex,NULL);pthread_mutex_init(&_cons_mutex,NULL);}~RingQueue(){sem_destroy(&_empty);sem_destroy(&_full);pthread_mutex_destroy(&_prod_mutex);pthread_mutex_destroy(&_cons_mutex);}// 生产者入队voidEnqueue(const T& data){sem_wait(&_empty);// P操作:空闲空间-1,无空间则阻塞pthread_mutex_lock(&_prod_mutex);// 多个生产者互斥// 生产数据 _queue[_prod_idx]= data; _prod_idx =(_prod_idx +1)% _cap;// 环形索引pthread_mutex_unlock(&_prod_mutex);sem_post(&_full);// V操作:已用空间+1,唤醒消费者}// 消费者出队boolDequeue(T& data){sem_wait(&_full);// P操作:已用空间-1,无数据则阻塞pthread_mutex_lock(&_cons_mutex);// 多个消费者互斥// 消费数据 data = _queue[_cons_idx]; _cons_idx =(_cons_idx +1)% _cap;pthread_mutex_unlock(&_cons_mutex);sem_post(&_empty);// V操作:空闲空间+1,唤醒生产者returntrue;}private: std::vector<T> _queue;// 环形队列数组int _cap;// 固定容量int _prod_idx;// 生产者索引int _cons_idx;// 消费者索引 sem_t _empty;// 空闲空间信号量 sem_t _full;// 已用空间信号量 pthread_mutex_t _prod_mutex;// 生产者互斥量 pthread_mutex_t _cons_mutex;// 消费者互斥量};}测试代码:
#include<iostream>#include<unistd.h>// 用于sleep#include<cstdlib>// 用于rand、srand#include<ctime>// 用于time(设置随机数种子)#include"RingQueue.hpp"// 你的环形队列头文件#include"Lock.hpp"// 你的互斥锁头文件#include"Cond.hpp"// 你的条件变量头文件usingnamespace std;usingnamespace Model;// 测试数据量(可调整)constint TEST_DATA_COUNT =20;// 生产者线程函数:向环形队列中写入数据void*ProducerThread(void* arg){ RingQueue<int>* rq =static_cast<RingQueue<int>*>(arg);srand(time(nullptr));// 设置随机数种子(仅生产者设置一次即可)for(int i =0; i < TEST_DATA_COUNT;++i){// 生成1~100的随机数作为测试数据int data =rand()%100+1;// 入队 rq->Enqueue(data); cout <<"[生产者] 写入数据: "<< data << endl;// 模拟生产耗时(100~300毫秒),让测试更直观usleep(rand()%200000+100000);} cout <<"[生产者] 数据写入完成,退出线程"<< endl;pthread_exit(nullptr);}// 消费者线程函数:从环形队列中读取数据void*ConsumerThread(void* arg){ RingQueue<int>* rq =static_cast<RingQueue<int>*>(arg);int data =0;for(int i =0; i < TEST_DATA_COUNT;++i){// 出队bool ret = rq->Dequeue(data);if(ret){ cout <<"[消费者] 读取数据: "<< data << endl;}else{ cout <<"[消费者] 出队失败!"<< endl;}// 模拟消费耗时(150~400毫秒),与生产耗时错开,体现同步usleep(rand()%250000+150000);} cout <<"[消费者] 数据读取完成,退出线程"<< endl;pthread_exit(nullptr);}intmain(){// 创建环形队列(容量设为5,测试环形特性) RingQueue<int>ring_queue(5); pthread_t prod_tid, cons_tid;int ret =0;// 创建生产者线程 ret =pthread_create(&prod_tid,nullptr, ProducerThread,&ring_queue);if(ret !=0){ cerr <<"创建生产者线程失败!错误码: "<< ret << endl;return-1;}// 创建消费者线程 ret =pthread_create(&cons_tid,nullptr, ConsumerThread,&ring_queue);if(ret !=0){ cerr <<"创建消费者线程失败!错误码: "<< ret << endl;return-1;}// 等待两个线程结束 ret =pthread_join(prod_tid,nullptr);if(ret !=0){ cerr <<"等待生产者线程失败!错误码: "<< ret << endl;return-1;} ret =pthread_join(cons_tid,nullptr);if(ret !=0){ cerr <<"等待消费者线程失败!错误码: "<< ret << endl;return-1;} cout <<"\n===== 单生产者单消费者测试完成 ====="<< endl;return0;}
4.4 两种模型对比与选型
| 特性 | 阻塞队列(动态) | 环形队列(固定) |
|---|---|---|
| 空间大小 | 动态扩容,无固定限制 | 固定大小,需提前预估 |
| 实现方式 | 互斥量+条件变量 | 互斥量+信号量 |
| 性能 | 扩容时有开销,中等 | 无扩容开销,高性能 |
| 适用场景 | 数据量不确定,追求灵活性 | 数据量稳定,追求高并发 |
五、线程安全与可重入问题
线程安全和可重入是多线程编程中容易混淆的概念,需明确区分:
5.1 线程安全:多线程访问的一致性保障
- 定义:多个线程并发访问同一资源时,结果始终符合预期,无数据竞争;
- 线程安全的场景:
- 仅访问局部变量(每个线程有独立栈空间);
- 访问共享资源但有互斥保护(如加锁);
- 仅读取共享资源(无写入操作);
- 线程不安全的场景:
- 无保护地修改全局变量/静态变量;
- 函数状态随调用变化(如返回静态变量指针);
- 调用线程不安全的函数(如
strtok)。
5.2 可重入函数:重入场景下的正确性
- 定义:同一函数被多个执行流(线程/信号处理函数)重入时,运行结果正确;
- 可重入的场景:
- 不使用全局变量/静态变量;
- 不调用
malloc/free(依赖全局堆管理); - 不调用标准I/O库函数(依赖全局缓冲区);
- 所有数据来自参数或局部变量;
- 不可重入的场景:
- 使用全局变量/静态变量;
- 调用不可重入函数(如
malloc、printf); - 返回静态变量指针(如
char* get_str() { static char buf[1024]; return buf; })。
5.3 核心区别与联系
| 维度 | 线程安全 | 可重入 |
|---|---|---|
| 关注对象 | 多线程并发访问的资源一致性 | 函数自身的重入安全性 |
| 依赖条件 | 可能依赖互斥锁 | 不依赖任何共享资源 |
| 关系 | 可重入函数一定是线程安全的 | 线程安全函数不一定是可重入的 |
| 示例 | 加锁的全局变量操作 | 仅使用局部变量的函数 |
六、死锁
死锁是多线程编程的常见问题,指多个线程互相持有对方需要的锁,且均不释放,导致永久阻塞。
6.1 死锁的四个必要条件
- 互斥条件:资源只能被一个线程持有;
- 请求与保持条件:线程持有部分资源,同时请求其他线程的资源;
- 不剥夺条件:资源不能被强行剥夺,只能由持有者主动释放;
- 循环等待条件:线程间形成“线程A(持lock1,等lock2)→ 线程B(持lock2,等lock3)→ 线程C(持lock3,等lock1)→ 线程A”的循环。
6.2 避免死锁的实战方法
方法1:破坏循环等待条件(最常用)
- 给锁编号,所有线程按统一顺序加锁;
- 示例:线程A和B都需要锁1和锁2,约定先加锁1,再加锁2:
// 正确:按顺序加锁pthread_mutex_lock(&lock1);pthread_mutex_lock(&lock2);// 操作资源pthread_mutex_unlock(&lock2);pthread_mutex_unlock(&lock1);方法2:破坏请求与保持条件
- 一次性申请所有需要的资源,申请失败则释放已申请的资源;
- 示例:使用
pthread_mutex_trylock尝试加锁,失败则回滚:
if(pthread_mutex_lock(&lock1)!=0){return-1;}if(pthread_mutex_trylock(&lock2)!=0){pthread_mutex_unlock(&lock1);// 回滚已加的锁return-1;}方法3:设置锁超时
- 使用
pthread_mutex_timedlock,超时则释放锁并重试; - 避免线程永久阻塞。
方法4:破坏不剥夺条件
- 允许线程在超时后释放已持有的锁(需配合业务逻辑设计)。
七、总结
- 互斥:用互斥量(mutex)保护临界区,RAII封装避免锁泄漏;
- 同步:用条件变量(cond)实现线程有序协作,必须配合互斥量;
- 生产者消费者模型:阻塞队列(灵活)和环形队列(高性能),解耦并发角色;
- 线程池:预创建线程,避免线程频繁创建销毁,提升高并发性能;
- 避坑:区分线程安全与可重入,避免死锁。