Linux线程同步与互斥
一、核心理论基础:互斥与同步
1. 互斥(Mutex):临界资源的排他性访问
核心概念
- 临界资源:多线程中需共同读写的资源(如全局变量、文件、硬件设备),同一时刻只能被一个线程访问。
- 互斥:通过锁机制保证临界资源的排他性访问,避免多个线程同时操作导致数据不一致。
- 原子操作:加锁后到解锁前的代码段,必须在一次线程调度中完整执行,不可被其他线程打断。
问题根源:指令穿插执行
以A++为例,其汇编指令至少包含3步:
- 读取变量A的值到寄存器;
- 寄存器中值+1;
- 将结果写回变量A。
若线程1执行完前2步被调度切换,线程2继续操作A,会导致最终结果小于预期(数据一致性破坏)。
互斥锁使用步骤
- 定义互斥锁:
pthread_mutex_t mutex; - 初始化锁:
pthread_mutex_init(&mutex, NULL);(NULL表示默认属性) - 加锁:
pthread_mutex_lock(&mutex);(阻塞式,若锁被占用则等待) - 解锁:
pthread_mutex_unlock(&mutex);(释放锁,唤醒等待线程) - 销毁锁:
pthread_mutex_destroy(&mutex);(资源释放,避免内存泄漏)
关键函数说明
| 函数原型 | 功能描述 |
|---|---|
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr) | 初始化互斥锁,attr为锁属性(默认NULL) |
int pthread_mutex_lock(pthread_mutex_t *mutex) | 阻塞加锁,若锁已被占用则线程阻塞 |
int pthread_mutex_trylock(pthread_mutex_t *mutex) | 非阻塞加锁,锁被占用时直接返回错误,不阻塞 |
int pthread_mutex_unlock(pthread_mutex_t *mutex) | 解锁,必须由加锁线程执行 |
int pthread_mutex_destroy(pthread_mutex_t *mutex) | 销毁互斥锁,需在解锁后执行 |
2. 同步(Semaphore):有顺序的资源访问
核心概念
- 同步:多线程按预定顺序执行,本质是“带顺序约束的互斥”,属于互斥的特例。
- 信号量:通过计数器控制资源访问权限,支持线程间交叉释放(如线程1释放信号量唤醒线程2)。
信号量使用步骤
- 定义信号量:
sem_t sem; - 初始化信号量:
sem_init(&sem, pshared, value);pshared=0:线程间使用;pshared!=0:进程间使用;value:信号量初始值(二值信号量为0/1,计数信号量可大于1)。
- P操作(申请资源):
sem_wait(&sem);(信号量-1,为0则阻塞) - V操作(释放资源):
sem_post(&sem);(信号量+1,唤醒阻塞线程) - 销毁信号量:
sem_destroy(&sem);
3. 互斥锁与信号量的区别
| 对比维度 | 互斥锁 | 信号量 |
|---|---|---|
| 释放主体 | 必须由加锁线程释放 | 可由其他线程释放(交叉唤醒) |
| 资源计数 | 仅支持二值(0/1,独占资源) | 支持计数(≥0,多资源共享) |
| 适用场景 | 临界资源排他访问(单资源) | 线程同步、多资源并发访问 |
| 休眠允许 | 临界区不可休眠(避免死锁) | 可适当休眠(如等待资源) |
4. 死锁:多线程的“致命陷阱”
死锁的四个必要条件(缺一不可)
- 互斥条件:资源只能被一个线程占用;
- 请求与保持:线程持有部分资源,同时请求其他资源;
- 不剥夺条件:资源不可被强行剥夺,只能主动释放;
- 循环等待:多个线程形成资源请求循环(如线程1等线程2的资源,线程2等线程1的资源)。
二、实战代码解析:从问题到解决方案
示例1:无锁场景——数据竞争问题(01pthreadr.c)
代码功能
两个线程同时对全局变量A执行5000次自增,无锁保护,观察数据不一致问题。
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<pthread.h>intA=0;// 临界资源(全局变量)void*th(void*arg){inti=5000;while(i--){inttmp=A;// 步骤1:读取Aprintf("A is %d\n",tmp+1);A=tmp+1;// 步骤3:写回A}returnNULL;}intmain(intargc,char**argv){pthread_ttid1,tid2;pthread_create(&tid1,NULL,th,NULL);pthread_create(&tid2,NULL,th,NULL);pthread_join(tid1,NULL);pthread_join(tid2,NULL);return0;}运行结果与问题
- 预期结果:A最终为10000;
- 实际结果:A通常小于10000(如9876),且每次运行结果不同。
- 原因:两个线程的
A++指令穿插执行,导致数据覆盖。
示例2:互斥锁解决数据竞争(02pthread_10000.c)
代码功能
在示例1基础上添加互斥锁,保护全局变量A的自增操作,保证数据一致性。
#include<string.h>#include<stdio.h>#include<pthread.h>#include<stdlib.h>#include<unistd.h>intA=0;pthread_mutex_tmutex;// 定义互斥锁void*thread(void*arg){inti=5000;while(i--){pthread_mutex_lock(&mutex);// 加锁(临界区开始)inttemp=A;printf("A is %d\n",temp+1);A=temp+1;pthread_mutex_unlock(&mutex);// 解锁(临界区结束)}returnNULL;}intmain(){pthread_tt1,t2;pthread_mutex_init(&mutex,NULL);// 初始化锁pthread_create(&t1,NULL,thread,NULL);pthread_create(&t2,NULL,thread,NULL);pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_mutex_destroy(&mutex);// 销毁锁printf("A is %d\n",A);// 最终结果稳定为10000return0;}关键改进
- 加锁后,
A++的三步操作成为原子操作,避免线程穿插; - 运行结果:A最终稳定为10000,数据一致性得到保证。
- 编译命令:
gcc 02pthread_10000.c -o lock_demo -lpthread
示例3:互斥锁错误使用——死锁风险(03lock.c)
代码功能
10个线程竞争3个柜台,每个线程依次申请所有柜台锁,演示死锁场景。
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<unistd.h>#include<time.h>#include<string.h>#defineWIN3// 3个柜台#defineNUM_THREADS10// 10个线程pthread_mutex_tcounter_locks[WIN];// 柜台锁数组typedefstruct{intclient_id;// 线程编号}client_t;void*client(void*arg){client_t*client=(client_t*)arg;intid=client->client_id;inti=0;// 错误:每个线程依次申请所有柜台锁for(i=0;i<WIN;i++){pthread_mutex_lock(&counter_locks[i]);printf("Client %d is in critical section (win%d)\n",id,i+1);sleep(1);// 临界区休眠,放大死锁概率pthread_mutex_unlock(&counter_locks[i]);}pthread_exit(NULL);}intmain(){inti=-1;pthread_tthreads[NUM_THREADS];client_t*clients=malloc(sizeof(client_t)*NUM_THREADS);// 初始化柜台锁for(i=0;i<WIN;i++)pthread_mutex_init(&counter_locks[i],NULL);// 创建10个线程for(i=0;i<NUM_THREADS;i++){clients[i].client_id=i;pthread_create(&threads[i],NULL,client,(void*)&clients[i]);}// 回收线程for(i=0;i<NUM_THREADS;i++){pthread_join(threads[i],NULL);}// 销毁锁for(i=0;i<WIN;i++)pthread_mutex_destroy(&counter_locks[i]);free(clients);return0;}死锁原因
- 线程1持有win1锁,申请win2锁;线程2持有win2锁,申请win1锁,形成循环等待;
- 临界区包含
sleep(1),延长锁持有时间,死锁概率极高。 - 解决思路:所有线程按固定顺序申请锁(如统一先申请win1,再win2、win3),打破循环等待条件。
示例4:非阻塞加锁——避免死锁(04trylock.c)
代码功能
10个线程竞争3个柜台,使用pthread_mutex_trylock非阻塞加锁,避免死锁。
#include<pthread.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include<unistd.h>pthread_mutex_tmutex1,mutex2,mutex3;// 3个柜台锁void*th(void*arg){intret=0;while(1){// 非阻塞申请win1锁ret=pthread_mutex_trylock(&mutex1);if(0==ret){printf("get win1...\n");sleep(rand()%5+1);// 模拟业务办理printf("release win1...\n");pthread_mutex_unlock(&mutex1);break;}// 申请win1失败,尝试win2elseif((ret=pthread_mutex_trylock(&mutex2))==0){printf("get win2...\n");sleep(rand()%5+1);printf("release win2...\n");pthread_mutex_unlock(&mutex2);break;}// 申请win2失败,尝试win3elseif((ret=pthread_mutex_trylock(&mutex3))==0){printf("get win3...\n");sleep(rand()%5+1);printf("release win3...\n");pthread_mutex_unlock(&mutex3);break;}}returnNULL;}intmain(intargc,char**argv){inti=0;srand(time(NULL));pthread_ttid[10]={0};// 初始化锁pthread_mutex_init(&mutex1,NULL);pthread_mutex_init(&mutex2,NULL);pthread_mutex_init(&mutex3,NULL);// 创建10个线程for(i=0;i<10;i++){pthread_create(&tid[i],NULL,th,NULL);}// 回收线程for(i=0;i<10;i++){pthread_join(tid[i],NULL);}// 销毁锁pthread_mutex_destroy(&mutex1);pthread_mutex_destroy(&mutex2);pthread_mutex_destroy(&mutex3);return0;}核心优化
- 使用
pthread_mutex_trylock非阻塞加锁,申请失败时立即尝试下一个资源,不阻塞; - 避免线程间循环等待,彻底解决死锁问题;
- 运行结果:10个线程依次抢占3个柜台,无死锁,正常释放资源。
示例5:信号量实现线程同步(05sem_hw.c)
代码功能
两个线程通过信号量实现同步,交替输出“Hello”和“World”(线程1输出后唤醒线程2,线程2输出后唤醒线程1)。
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<pthread.h>#include<unistd.h>#include<time.h>#include<semaphore.h>sem_tsem_H,sem_W;// 信号量:sem_H控制Hello,sem_W控制Worldvoid*th1(void*arg){inti=10;while(i--){sem_wait(&sem_H);// P操作:申请Hello信号量(初始为1)printf("Hello\n");fflush(stdout);// 刷新缓冲区,避免输出乱序sem_post(&sem_W);// V操作:释放World信号量(唤醒线程2)}returnNULL;}void*th2(void*arg){inti=10;while(i--){sem_wait(&sem_W);// P操作:申请World信号量(初始为0,阻塞)printf("World\n");sleep(1);// 模拟耗时操作sem_post(&sem_H);// V操作:释放Hello信号量(唤醒线程1)}returnNULL;}intmain(){pthread_ttid1,tid2;// 初始化信号量:sem_H=1(线程1可先执行),sem_W=0(线程2阻塞)sem_init(&sem_H,0,1);sem_init(&sem_W,0,0);pthread_create(&tid1,NULL,th1,NULL);pthread_create(&tid2,NULL,th2,NULL);pthread_join(tid1,NULL);pthread_join(tid2,NULL);// 销毁信号量sem_destroy(&sem_H);sem_destroy(&sem_W);return0;}同步逻辑
- 初始状态:
sem_H=1,sem_W=0; - 线程1申请
sem_H成功,输出“Hello”,释放sem_W(sem_W=1); - 线程2申请
sem_W成功,输出“World”,释放sem_H(sem_H=1); - 循环10次,实现“Hello→World”交替输出。
示例6:计数信号量——多资源共享(06semcount.c)
代码功能
10个线程竞争3个柜台(多资源),使用计数信号量控制并发访问(最多3个线程同时占用资源)。
#include<pthread.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include<unistd.h>#include<semaphore.h>sem_tsem_WIN;// 计数信号量(控制柜台资源)void*th(void*arg){sem_wait(&sem_WIN);// P操作:申请柜台资源(信号量-1)printf("get win...\n");sleep(rand()%5+1);// 模拟业务办理(随机耗时1-5秒)printf("release win...\n");sem_post(&sem_WIN);// V操作:释放柜台资源(信号量+1)returnNULL;}intmain(intargc,char**argv){inti=0;srand(time(NULL));pthread_ttid[10]={0};// 初始化计数信号量:初始值3(3个柜台资源)sem_init(&sem_WIN,0,3);// 创建10个线程for(i=0;i<10;i++){pthread_create(&tid[i],NULL,th,NULL);}// 回收线程for(i=0;i<10;i++){pthread_join(tid[i],NULL);}// 销毁信号量sem_destroy(&sem_WIN);return0;}核心逻辑
- 信号量初始值为3,表示最多3个线程同时占用柜台;
- 线程申请资源时
sem_WIN-1,释放时sem_WIN+1; - 当
sem_WIN=0时,后续线程阻塞,直到有线程释放资源; - 运行结果:始终最多3个线程同时办理业务,无资源竞争。
三、关键注意事项与避坑指南
1. 互斥锁使用规范
- 临界区最小化:加锁后仅包含必要的操作(如读写临界资源),避免休眠、耗时操作(如
sleep、网络请求); - 加解锁成对出现:避免“加锁后未解锁”导致死锁,或“解锁未加锁”导致崩溃;
- 统一锁顺序:多锁场景下,所有线程按固定顺序申请锁(如先锁A再锁B),打破循环等待条件。
2. 信号量使用规范
- 区分二值与计数信号量:二值信号量(0/1)用于互斥,计数信号量(≥1)用于多资源共享;
- 信号量初始值合理设置:根据资源数量设置(如3个柜台则初始值为3);
- 避免信号量泄露:申请资源后必须释放,即使线程异常退出(可通过
pthread_cleanup_push注册清理函数)。
3. 死锁预防方法
- 破坏四个必要条件之一即可:
- 避免循环等待(统一锁顺序);
- 避免请求与保持(一次性申请所有资源);
- 允许资源剥夺(如使用非阻塞加锁);
- 弱化互斥条件(如使用读写锁,允许多读单写)。
四、总结:互斥与同步的适用场景
| 场景 | 推荐方案 | 核心原因 |
|---|---|---|
| 单资源排他访问(如全局变量) | 互斥锁 | 实现简单,效率高 |
| 多资源并发访问(如多个柜台) | 计数信号量 | 支持资源数量控制 |
| 线程间顺序依赖(如A执行后B才能执行) | 信号量 | 支持交叉唤醒,同步灵活 |
| 避免死锁的抢占场景 | 非阻塞加锁(trylock) | 申请失败时可尝试其他资源 |