线程的互斥和同步是多线程编程的核心问题,用于解决资源竞争和执行时序协调的问题,确保多线程程序的正确性、稳定性和可预测性。
核心概念铺垫
- 临界区(Critical Section):多个线程共享的资源(如全局变量、硬件设备、文件句柄)或操作这些资源的代码段,同一时间只能被一个线程执行,否则会引发数据错乱(如 “脏读”“重复写”)。
- 竞态条件(Race Condition):多个线程同时访问临界区,且执行顺序不可控,导致程序输出结果依赖于线程调度顺序的错误现象(是互斥要解决的核心问题)。
互斥
1.定义
互斥是指:禁止多个线程同时进入同一临界区,保证临界区的 “排他性访问”,本质是解决 “资源竞争” 问题。
简单说:互斥是 “不许同时干”,核心是 “抢资源” 的问题。
2. 常见实现方式
(1)互斥锁(Mutex)
最常用的互斥机制,本质是一个 “锁标记”:
- 线程进入临界区前加锁(
lock):如果锁未被占用,成功加锁并进入;如果已被占用,线程阻塞等待,直到锁被释放。 - 线程离开临界区后解锁(
unlock):释放锁,让等待的线程竞争获取。 - 互斥的使用步骤:定义互斥锁 ==》初始化锁==》加锁==》解锁==》销毁
特性:
- 互斥锁是 “非递归” 的(默认):同一线程重复加锁会导致死锁。
- 支持 “公平 / 非公平” 调度:公平锁按等待顺序唤醒线程,非公平锁随机唤醒(效率更高)。
(2)自旋锁(Spin Lock)
与互斥锁的区别:线程获取不到锁时,不阻塞,而是循环(自旋)检查锁是否释放,直到获取到锁。
- 适用场景:临界区执行时间极短(如几纳秒),避免线程上下文切换的开销。
- 缺点:自旋会占用 CPU 资源,临界区耗时过长时会导致 CPU 利用率飙升。
(3)其他互斥机制
- 信号量(Semaphore):初始值为 1 的信号量可作为互斥锁(二值信号量);
- 原子操作(Atomic):对简单数据类型(如 int、bool)的操作,通过 CPU 指令保证原子性(无需加锁,效率更高)。
特性:
- 互斥锁是 “非递归” 的(默认):同一线程重复加锁会导致死锁。
- 支持 “公平 / 非公平” 调度:公平锁按等待顺序唤醒线程,非公平锁随机唤醒(效率更高)。
相关函数
1、定义:
pthread_mutex_t mutex;
2、初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
- mutex要初始化的互斥锁
- atrr初始化的值,一般是NULL表示默认锁
- 成功0
- 失败 非零
3、加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 用指定的互斥锁开始加锁代码
- 加锁后的代码到解锁部分的代码属于***原子操作***,
- 在加锁期间其他进程/线程都不能操作该部分代码
- 如果该函数在执行的时候,mutex已经被其他部分使用则代码阻塞。
- 成功0
- 失败 非零
4、解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 将指定的互斥锁解锁。
- 解锁之后代码不再排他访问,一般加锁解锁同时出现。
- 成功0
- 失败 非零
5、销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 成功0
- 失败 非零
同步
1. 定义
同步是指:协调多个线程的执行顺序,让线程按预期的时序执行(比如 “A 线程执行完某步后,B 线程才能执行”),本质是解决 “执行时序” 问题。
简单说:同步是 “按顺序干”,核心是 “等通知” 的问题。
2. 常见实现方式
(1)条件变量(Condition Variable)
条件变量结合互斥锁使用,实现 “线程等待某个条件满足后再执行”。
- 核心操作:wait()(线程阻塞,释放锁)、notify_one()/notify_all()(唤醒等待的线程)。
(2)信号量(Semaphore)
信号量可实现更灵活的同步:
- 计数信号量:初始值为 N,表示最多允许 N 个线程同时访问资源;
- 同步信号量:初始值为 0,实现 “生产者 - 消费者” 等时序协调(生产者生产后post,消费者wait后消费)。
(3)屏障(Barrier)
屏障让多个线程在某个点 “同步等待”,直到所有线程都到达该点后,再继续执行。
- 适用场景:多线程分阶段任务(如 “所有线程完成初始化后,再执行核心逻辑”)。
相关函数
1、信号量的定义 :
sem_t sem;
2、信号量的初始化:
int sem_init(sem_t *sem, int pshared, unsigned int value);
- sem要初始化的信号量
- pshared = 0 ;表示线程间使用信号量
- pshared != 0 ;表示进程间使用信号量
- value信号量的初始值,一般无名信号量
- 都是二值信号量,0 1
- 0表示红灯,进程暂停阻塞
- 1表示绿灯,进程可以通过执行
- 成功0
- 失败-1;
3、信号量的PV操作
int sem_wait(sem_t *sem);
- 成功0
- 失败-1
int sem_post(sem_t *sem);
- 成功0
- 失败-1;
int sem_destroy(sem_t *sem);
- 成功0
- 失败-1;
互斥和同步的区别
| 互斥 | 同步 | |
| 核心目标 | 解决资源竞争(排他访问) | 解决执行时序(协调顺序) |
| 关注点 | 临界区的 “独占性” | 线程间的 “依赖性” |
| 典型场景 | 多线程修改同一变量 | 生产者 - 消费者、等待通知 |
| 实现基础 | 互斥锁、自旋锁、原子操作 | 条件变量、信号量、屏障 |
死锁
定义
产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。