线程的互斥和同步是多线程编程的核心问题,用于解决资源竞争和执行时序协调的问题,确保多线程程序的正确性、稳定性和可预测性。
核心概念铺垫
- 临界区(Critical Section):多个线程共享的资源(如全局变量、硬件设备、文件句柄)或操作这些资源的代码段,同一时间只能被一个线程执行,否则会引发数据错乱(如 “脏读”“重复写”)。
- 竞态条件(Race Condition):多个线程同时访问临界区,且执行顺序不可控,导致程序输出结果依赖于线程调度顺序的错误现象(是互斥要解决的核心问题)。
互斥
1.定义
互斥是指:禁止多个线程同时进入同一临界区,保证临界区的 “排他性访问”,本质是解决 “资源竞争” 问题。
简单说:互斥是 “不许同时干”,核心是 “抢资源” 的问题。
2. 常见实现方式
(1)互斥锁(Mutex)
最常用的互斥机制,本质是一个 “锁标记”:
特性:
- 互斥锁是 “非递归” 的(默认):同一线程重复加锁会导致死锁。
- 支持 “公平 / 非公平” 调度:公平锁按等待顺序唤醒线程,非公平锁随机唤醒(效率更高)。
(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表示默认锁
返回值:
3、加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
- 用指定的互斥锁开始加锁代码
- 加锁后的代码到解锁部分的代码属于***原子操作***,
- 在加锁期间其他进程/线程都不能操作该部分代码
- 如果该函数在执行的时候,mutex已经被其他部分使用则代码阻塞。
参数:mutex用来给代码加锁的互斥锁
返回值:
原子操作: 在线程的一次调度中,这段代码必须完成,不能发生线程调度。
4、解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
- 将指定的互斥锁解锁。
- 解锁之后代码不再排他访问,一般加锁解锁同时出现。
参数:用来解锁的互斥锁
返回值:
5、销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:使用互斥锁完毕后需要销毁互斥锁
参数:mutex要销毁的互斥锁
返回值:
同步
1. 定义
同步是指:协调多个线程的执行顺序,让线程按预期的时序执行(比如 “A 线程执行完某步后,B 线程才能执行”),本质是解决 “执行时序” 问题。
简单说:同步是 “按顺序干”,核心是 “等通知” 的问题。
步骤:信号量的定义 =》信号量的初始化==》信号量的PV操作=》信号量的销毁。
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表示绿灯,进程可以通过执行
返回值:
3、信号量的PV操作
![]()
P ===》申请资源===》申请一个二值信号量
V ===》释放资源===》释放一个二值信号量
P操作对应函数==》sem_wait();
V操作对应函数==》sem_post();
int sem_wait(sem_t *sem);
功能:
判断当前sem信号量是否有资源可用。如果sem有资源(==1),则申请该资源,程序继续运行 。如果sem没有资源(==0),则线程阻塞等待,一旦有资源则自动申请资源并继续运行程序。
注意:sem申请资源后会自动执行sem = sem - 1;
参数:sem要判断的信号量资源
返回值:
int sem_post(sem_t *sem);
功能:函数可以将指定的sem信号量资源释放并默认执行,sem = sem+1; 线程在该函数上不会阻塞。
参数:sem要释放资源的信号量
返回值:
4、信号量的销毁
int sem_destroy(sem_t *sem);
功能:使用完毕将指定的信号量销毁
参数:sem要销毁的信号量
返回值:
互斥和同步的区别
1.互斥锁,加锁和解锁 是同一个线程
信号量(同步),th1释放th2, th2释放th1.是由线程交叉释放。
2.在互斥锁保护的代码中(临界区)。不要休眠,不要大耗时的操作。临界区代码短小精悍
信号量,适当可以有休眠,小的耗时操作。
用法: 计数信号量 。信号量的初值(3,5)是可以大于1的。 这种情况,用于互斥的情况,资源数本身不唯一(多个资源);
| 互斥 | 同步 |
| 核心目标 | 解决资源竞争(排他访问) | 解决执行时序(协调顺序) |
| 关注点 | 临界区的 “独占性” | 线程间的 “依赖性” |
| 典型场景 | 多线程修改同一变量 | 生产者 - 消费者、等待通知 |
| 实现基础 | 互斥锁、自旋锁、原子操作 | 条件变量、信号量、屏障 |
死锁
定义
由于锁资源安排的不合理(锁资源的申请和释放逻辑不对),导致进程,线程无法正常继续执行(推
进)的现象。
产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。