一、条件变量的核心概念
1. 为什么需要条件变量?
互斥锁解决了 "多线程互斥访问共享资源" 的问题,但无法解决 "线程需要等待某个条件满足后再执行" 的场景:
- 比如:生产者线程生产数据,消费者线程需要等数据生产完成后才能消费;
- 若只用互斥锁,消费者只能 "轮询检查"(不停加锁→判断条件→解锁),会浪费大量 CPU 资源;
- 条件变量的作用 :让线程在条件不满足时阻塞等待 ,条件满足时被唤醒,避免无效轮询,提升效率。
2. 条件变量的核心特性
- 依赖互斥锁:条件变量必须和互斥锁配合使用(保护条件判断的原子性);
- 等待 / 唤醒机制 :
- 等待:线程调用
pthread_cond_wait()时,会自动释放互斥锁并阻塞; - 唤醒:其他线程调用
pthread_cond_signal()/pthread_cond_broadcast()唤醒等待的线程,被唤醒的线程会重新获取互斥锁;
- 等待:线程调用
- 虚假唤醒 :线程可能在没有被显式唤醒的情况下醒来(内核调度原因),因此条件判断必须用
while而非if; - 无所有权:条件变量本身不保护资源,只负责 "等待 / 唤醒",资源保护仍需互斥锁。
3. 形象比喻
条件变量 = 公司的 "等待区" + "通知铃":
- 互斥锁 = 等待区的门(保证同一时间只有一个人进等待区);
- 线程(员工):想办事但条件不满足→进等待区(
pthread_cond_wait)→自动开门(释放锁)→坐等通知; - 其他线程(管理员):条件满足→按铃(
pthread_cond_signal)→唤醒等待的员工; - 被唤醒的员工:重新抢门(获取锁)→再次检查条件→条件满足则办事,不满足则继续等(解决虚假唤醒)。
二、条件变量核心函数(pthread 库)
Linux 条件变量的类型是 pthread_cond_t,所有函数都依赖 pthread 库(编译加 -lpthread)。
1. 条件变量初始化:pthread_cond_init()
功能:初始化一个条件变量。
函数原型:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
参数:
cond:指向 pthread_cond_t 类型的指针,用于存储条件变量。attr:指向 pthread_condattr_t 类型的指针,用于指定条件变量属性。通常设置为 NULL 表示使用默认属性。- 返回值:成功返回 0,失败返回错误码。
2. 等待条件满足:pthread_cond_wait()
功能 :阻塞等待条件变量被唤醒,且会自动释放关联的互斥锁(核心函数)。
函数原型:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:
cond:要等待的条件变量;mutex:关联的互斥锁;- 核心逻辑(原子操作,不可分割):
- 释放传入的互斥锁
mutex; - 阻塞当前线程,等待被唤醒;
- 被唤醒后,重新获取互斥锁
mutex; - 函数返回,线程继续执行。
- 返回值:成功返回 0,失败返回错误码。
3. 限时等待:pthread_cond_timedwait()
功能 :和 pthread_cond_wait() 类似,但增加了超时时间,超时后自动返回。
函数原型:
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
参数:
cond:要等待的条件变量;mutex:关联的互斥锁;
abstime:绝对超时时间(不是相对时间),格式为struct timespec(秒 + 纳秒);
4. 唤醒线程:pthread_cond_signal()
功能 :唤醒至少一个等待该条件变量的线程(通常唤醒队列第一个)。
函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
注意:
- 唤醒后线程不会立即执行,需重新获取互斥锁;
- 若没有线程等待,该函数无效果(不会报错)。
5. 唤醒所有线程:pthread_cond_broadcast()
功能 :唤醒所有等待该条件变量的线程。
函数原型:
int pthread_cond_broadcast(pthread_cond_t *cond);
适用场景:多个线程等待同一条件,且条件满足后所有线程都需要执行(比如 "数据更新后,所有消费者都要处理")。
6. 销毁条件变量:pthread_cond_destroy()
功能:销毁条件变量,释放相关资源。
函数原型:
int pthread_cond_destroy(pthread_cond_t *cond);
注意:
- 必须确保没有线程在等待该条件变量,否则返回
EBUSY错误; - 静态初始化的条件变量也可以调用该函数(无副作用)。
三、关键注意事项
1. 必须配合互斥锁使用
- 错误用法:直接调用
pthread_cond_wait不加锁 → 条件判断可能被打断,导致竞态条件; - 核心逻辑:
pthread_cond_wait的第一个参数是条件变量,第二个是互斥锁,缺一不可。
2. 条件判断必须用 while 而非 if
-
虚假唤醒:线程可能被内核随机唤醒(比如信号中断),此时条件并不满足;
-
用
if的问题:被虚假唤醒后直接执行后续逻辑,导致操作非法资源(比如消费空缓冲区); -
正确写法:
// 错误 if (count == 0) pthread_cond_wait(...); // 正确 while (count == 0) pthread_cond_wait(...);
3. 唤醒时机:解锁前 / 后?
pthread_cond_signal/broadcast可以在加锁时调用(示例中的写法),也可以在解锁后调用;- 加锁时唤醒:被唤醒的线程不会立即执行(需等锁释放),但逻辑更安全;
- 解锁后唤醒:避免 "唤醒丢失"(极少数场景),建议新手按示例写法(加锁时唤醒)。
4. 避免 "唤醒丢失"
- 场景:线程 A 检查条件不满足 → 准备调用
pthread_cond_wait→ 线程 B 此时满足条件并调用signal→ 线程 A 再调用wait→ 唤醒信号丢失,线程 A 永久阻塞; - 解决:条件判断和 wait 必须在锁的保护下(示例中已保证),避免上述时序问题。
5. 销毁条件变量的时机
- 必须等所有线程都不再使用该条件变量后销毁;
- 若有线程还在
wait中,调用pthread_cond_destroy会返回EBUSY错误,甚至导致崩溃。
四、条件变量 vs 互斥锁(核心区别)
| 特性 | 互斥锁(pthread_mutex_t) | 条件变量(pthread_cond_t) |
|---|---|---|
| 核心作用 | 互斥访问共享资源 | 等待 / 唤醒线程(基于条件) |
| 依赖关系 | 独立使用 | 必须配合互斥锁 |
| 阻塞方式 | 加锁时阻塞(抢锁失败) | 主动调用 wait 阻塞 |
| 唤醒方式 | 无(解锁后自动竞争) | signal/broadcast 主动唤醒 |
| 适用场景 | 保护共享资源 | 线程间同步(等待条件) |
五、总结
- 条件变量核心价值:解决 "线程等待条件" 的问题,避免无效轮询,提升 CPU 利用率;
- 核心函数 :
- 初始化:
pthread_cond_init/PTHREAD_COND_INITIALIZER; - 等待:
pthread_cond_wait(必带互斥锁); - 唤醒:
pthread_cond_signal(唤醒一个)/pthread_cond_broadcast(唤醒所有); - 销毁:
pthread_cond_destroy;
- 初始化:
- 关键规则 :
- 条件变量必须和互斥锁配合使用;
- 条件判断用
while而非if(解决虚假唤醒); - 典型场景:生产者 - 消费者模型、线程池、任务队列等;
- 核心逻辑:加锁→判断条件→wait(释放锁 + 阻塞)→被唤醒→重新加锁→再次判断条件→执行逻辑→解锁 + 唤醒。