一、互斥锁:临界资源的排他性访问
1.1 核心概念
(1)临界资源
多线程中会被读写操作的共享资源,常见类型:
- 全局变量、静态变量;
- 文件、设备(如串口、网卡);
- 其他可被多线程访问的共享内存 / 句柄。
(2)排他性访问
同一时刻,仅允许一个线程对临界资源进行读写操作 —— 这是互斥锁的核心目标。
(3)为什么需要互斥?
多线程并发执行时,代码是「穿插调度」的。以简单的a++为例:
c
运行
a++; // 看似一行代码,汇编至少分3步: // 1. 从内存读取a的值到寄存器; // 2. 寄存器中a的值+1; // 3. 寄存器值写回内存a。若线程 1 执行完前 2 步后被调度切走,线程 2 执行完整 3 步,再切回线程 1 执行第 3 步 —— 最终a只加了 1 次,而非预期的 2 次,导致数据一致性错误。
互斥锁的作用就是将这段代码变成「原子操作」:加锁后,这段代码必须在一次线程调度中完整执行,不可被打断。
1.2 互斥锁的使用步骤
互斥锁的生命周期遵循「定义→初始化→加锁→解锁→销毁」的固定流程,缺一不可。
(1)核心函数(POSIX 标准)
| 操作 | 函数原型 | 关键说明 |
|---|---|---|
| 定义 | pthread_mutex_t mutex; | 声明互斥锁变量(全局 / 局部均可,需保证所有线程可见) |
| 初始化 | int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); | attr 传 NULL 表示默认属性;成功返回 0,失败返回非 0 |
| 加锁 | int pthread_mutex_lock(pthread_mutex_t *mutex); | 加锁失败则阻塞(等待其他线程解锁);成功返回 0 |
| 解锁 | int pthread_mutex_unlock(pthread_mutex_t *mutex); | 必须与加锁线程为同一个;成功返回 0 |
| 销毁 | int pthread_mutex_destroy(pthread_mutex_t *mutex); | 锁未解锁时销毁会报错;成功返回 0 |
(2)核心规则
- 加锁和解锁必须由同一个线程执行;
- 临界区(加锁→解锁之间的代码)必须「短小精悍」:
- 禁止在临界区中执行
sleep、IO 等耗时操作; - 临界区代码越短,多线程并发效率越高。
- 禁止在临界区中执行
(3)基础示例:互斥锁保护全局变量
c
运行
#include <stdio.h> #include <pthread.h> #include <unistd.h> int a = 0; pthread_mutex_t mutex; void* th_func(void* arg) { for (int i = 0; i < 10000; i++) { pthread_mutex_lock(&mutex); // 加锁 a++; // 临界区:仅一行,原子操作 pthread_mutex_unlock(&mutex); // 解锁 } return NULL; } int main() { pthread_t tid1, tid2; pthread_mutex_init(&mutex, NULL); // 初始化锁 pthread_create(&tid1, NULL, th_func, NULL); pthread_create(&tid2, NULL, th_func, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&mutex); // 销毁锁 printf("最终a的值:%d\n", a); // 预期输出20000 return 0; }二、信号量:带顺序的同步访问
2.1 核心概念
(1)同步的定义
「有先后顺序的排他性访问」—— 不仅要保证同一时刻一个资源被一个线程访问,还要强制线程按照指定顺序执行(如线程 A 执行完后,线程 B 才能执行)。
(2)与互斥锁的关系
- 互斥包含同步:同步是互斥的「特例」(互斥只保证排他,同步额外保证顺序);
- 释放逻辑不同:
- 互斥锁:加锁 / 解锁是同一个线程;
- 信号量:线程 A 释放资源,线程 B 申请资源(交叉释放)。
(3)计数信号量的用途
信号量初始值可大于 1(如 3、5),适用于「多份相同资源」的竞争场景(如你之前代码中win=3的有限资源竞争)。
2.2 信号量的使用步骤
信号量生命周期:「定义→初始化→PV 操作→销毁」。
(1)核心函数(POSIX 标准)
| 操作 | 函数原型 | 关键说明 |
|---|---|---|
| 定义 | sem_t sem; | 声明信号量变量 |
| 初始化 | int sem_init(sem_t *sem, int pshared, unsigned int value); | pshared=0(线程间使用)、!=0(进程间);value = 初始资源数;成功返回 0,失败 - 1 |
| P 操作(申请资源) | int sem_wait(sem_t *sem); | sem>0 则 sem-1 并继续;sem=0 则阻塞;成功返回 0,失败 - 1 |
| V 操作(释放资源) | int sem_post(sem_t *sem); | sem+1,不阻塞;成功返回 0,失败 - 1 |
| 销毁 | int sem_destroy(sem_t *sem); | 成功返回 0,失败 - 1 |
(2)核心规则
- P 操作(
sem_wait):申请资源,资源数 - 1; - V 操作(
sem_post):释放资源,资源数 + 1; - 信号量临界区可包含短暂休眠 / 耗时操作(比互斥锁灵活)。
(3)基础示例:信号量实现线程同步
c
运行
#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> sem_t sem; // 信号量:控制线程执行顺序 void* th1(void* arg) { printf("线程1:执行初始化操作\n"); sleep(2); sem_post(&sem); // V操作:释放资源(sem=1) return NULL; } void* th2(void* arg) { sem_wait(&sem); // P操作:等待资源(sem=0时阻塞) printf("线程2:线程1初始化完成后执行\n"); return NULL; } int main() { pthread_t tid1, tid2; sem_init(&sem, 0, 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); return 0; }三、互斥锁 vs 信号量(核心区别)
| 维度 | 互斥锁 | 信号量 |
|---|---|---|
| 核心目标 | 排他性访问(无顺序) | 有序的排他性访问(同步) |
| 释放主体 | 加锁线程自己解锁 | 线程 A 申请,线程 B 释放(交叉) |
| 初始值 | 无(只有锁定 / 未锁定状态) | 可设为任意非负整数(如 0、1、3) |
| 临界区限制 | 禁止耗时操作,必须短小 | 可包含短暂休眠 / 小耗时操作 |
| 适用场景 | 单一资源的排他访问(如全局变量) | 多资源竞争、线程顺序控制 |
四、死锁:线程控制的 “致命陷阱”
4.1 死锁的定义
因锁资源的申请 / 释放逻辑错误,导致线程 / 进程永久阻塞,无法继续执行的现象。
4.2 死锁的四个必要条件(缺一不可)
- 互斥条件:一个资源每次只能被一个线程使用;
- 请求与保持条件:线程因申请资源阻塞时,不释放已持有的资源;
- 不剥夺条件:线程已获得的资源,未使用完前不能被强行剥夺;
- 循环等待条件:多个线程形成「A 等 B 的资源,B 等 C 的资源,C 等 A 的资源」的循环。
4.3 死锁规避技巧
- 锁的申请顺序一致:所有线程按「锁 1→锁 2→锁 3」的顺序申请,避免循环等待;
- 加锁限时:使用
pthread_mutex_timedlock替代pthread_mutex_lock,超时则放弃; - 避免嵌套锁:尽量减少锁的嵌套使用,嵌套越多,死锁风险越高;
- 及时解锁:临界区执行完毕立即解锁,不持有锁休眠。
五、实战场景:有限资源的多线程竞争
以你之前的「win=3 资源竞争」场景为例,用信号量替代互斥锁,更贴合 “多资源竞争” 的需求:
c
运行
#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> #include <stdlib.h> #include <time.h> int a = 0; sem_t sem; // 信号量:初始值3,代表3个可用资源 void* th_func(void* arg) { while (1) { sem_wait(&sem); // P操作:申请资源(资源数-1) // 临界区:可包含短暂休眠 printf("线程%lu:获取资源\n", pthread_self()); sleep(rand() % 2); a++; printf("线程%lu:释放资源,a=%d\n", pthread_self(), a); sem_post(&sem); // V操作:释放资源(资源数+1) if (a >= 10) break; // 退出条件 } return NULL; } int main() { pthread_t tid[10]; srand((unsigned int)time(NULL)); sem_init(&sem, 0, 3); // 初始化:线程间使用,初始资源数3 for (int i = 0; i < 10; i++) { pthread_create(&tid[i], NULL, th_func, NULL); } for (int i = 0; i < 10; i++) { pthread_join(tid[i], NULL); } sem_destroy(&sem); printf("最终a的值:%d\n", a); return 0; }六、核心总结
- 互斥锁:解决「单一资源的排他访问」,临界区必须短小,加解锁同线程;
- 信号量:解决「多资源竞争 / 线程同步」,支持交叉释放,临界区可适度耗时;
- 死锁规避:打破四个必要条件中的任意一个(如统一锁申请顺序)即可;
- 选型原则:
- 单一资源排他访问 → 互斥锁;
- 多资源竞争 / 线程顺序控制 → 信号量。