条件变量:线程的“等待队列 + 唤醒器”
条件变量是线程同步的重要工具,其核心在于维护一个“线程等待队列”的特殊临界资源。该工具的作用是使线程能够“有条件地等待”——当特定条件未达成(例如资源不足)时,线程会进入队列并休眠;一旦条件达成(如资源变得可用),其他线程将唤醒队列中的线程,使其恢复执行。
条件变量具备两个重要特性,且需与“互斥锁”协同工作:
- 队列管理: 确保多个等待线程能有序地进入休眠状态,防止无序等待。
- 唤醒控制: 支持选择性地唤醒单个线程或全部线程,从而灵活控制执行流程。
形象地说,条件变量类似于“公司会议室预订系统”。当会议室(临界资源)被占用时,希望开会的同事(线程)会在预订队列(条件变量)中等候。一旦会议室空出,管理员(其他线程)会通知队列中的同事“可以使用了”,随后这些同事便能开始使用会议室。
文档指出,条件变量所管理的队列必须通过互斥锁来保证线程在加入队列时的安全性,因为多个线程同时尝试进入等待队列可能会引发竞争条件,必须借助互斥锁来防止这种插队现象。
为何需要条件变量?——避免线程“盲目工作”
在条件变量问世之前,若线程需等待某一条件(如资源可用),通常只能采用“轮询”方式,即不断检查条件是否成立。然而,这种方法存在诸多弊端:
- 浪费CPU资源: 即使线程没有实际任务,也会频繁占用CPU来检查条件,类似于员工不断向领导询问是否有新任务,白白消耗资源。
- 效率低下: 若轮询频率过高,则浪费CPU;反之,若轮询间隔过长,则可能导致错过最佳执行时机。
- 无法有序等待: 多个线程同时进行轮询时,可能会同时发现条件满足,进而争抢资源,导致数据不一致的问题。
相比之下,条件变量有效解决了上述问题:
- 线程在等待期间会进入休眠状态,不再占用CPU资源。
- 当条件满足时,其他线程会主动唤醒等待中的线程,确保不错过任何机会。
- 与互斥锁配合使用,确保多个线程能够有序地等待和执行,有效避免竞争条件的发生。
如何使用条件变量?——掌握4步核心操作(附简化代码示例)
使用条件变量涉及“创建→等待→唤醒→销毁”四个基本步骤,且必须与互斥锁共同使用。以下是结合API和示例代码的详细说明,以“会议室预订”情景为例:
1. 核心API概览(参考文档版本)
| 操作 | 函数 | 功能描述 | 文档依据 |
|---|---|---|---|
| 创建/初始化 | |
定义条件变量(静态创建);或使用 动态初始化(NULL 表示使用默认属性) |
文档依据 |
| 线程等待 | |
线程释放互斥锁 → 进入等待队列休眠 → 被唤醒后重新获取互斥锁(全程确保安全) | 文档依据 |
| 唤醒线程 | |
唤醒等待队列中的第一个线程(适用于“一个资源释放,一个线程执行”场景) | 文档依据 |
| 唤醒所有线程 | |
唤醒等待队列中的所有线程(适用于“多个资源释放,多个线程执行”场景) | 文档依据 |
| 销毁 | |
释放条件变量资源,必须与初始化操作配对使用 | 文档依据 |
2. 实战代码(会议室预订场景)
cpp
// 示例代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 1. 定义条件变量(会议室预约队列)和互斥锁(防止插队)
pthread_cond_t meeting_cond; // 条件变量
pthread_mutex_t meeting_lock; // 互斥锁
int free_rooms = 0; // 空闲会议室数量(临界资源,初始0间)
// 线程1:员工(消费者)—— 申请会议室,没空闲则等
void* employee(void* arg) {
int id = *(int*)arg; // 员工ID
// 2. 加互斥锁:防止多个员工同时抢会议室(避免插队)
pthread_mutex_lock(&meeting_lock);
printf("员工%d:申请会议室,当前空闲:%d间\n", id, free_rooms);
// 3. 没空闲会议室→进入条件变量队列等待
while (free_rooms == 0) { // 用while!防止“虚假唤醒”(后面讲坑)
printf("员工%d:没会议室,排队等...\n", id);
pthread_cond_wait(&meeting_cond, &meeting_lock); // 释放锁→休眠→唤醒后重新加锁
printf("员工%d:被通知,检查会议室...\n", id);
}
// 4. 有空闲会议室→使用(操作临界资源)
free_rooms--;
printf("员工%d:占用会议室,剩余空闲:%d间\n", id, free_rooms);
pthread_mutex_unlock(&meeting_lock); // 释放互斥锁
// 模拟开会(耗时操作)
sleep(2);
printf("员工%d:会议结束,释放会议室\n", id);
// 5. 释放会议室→唤醒等待的员工
pthread_mutex_lock(&meeting_lock);
free_rooms++;
printf("员工%d:释放后空闲:%d间,通知下一个人\n", id, free_rooms);
pthread_cond_signal(&meeting_cond); // 唤醒队列第一个员工
pthread_mutex_unlock(&meeting_lock);
return NULL;
}
// 线程2:行政(生产者)—— 开放3间会议室
void* admin(void* arg) {
pthread_mutex_lock(&meeting_lock);
free_rooms = 3; // 开放3间会议室
printf("行政:开放3间会议室!\n");
pthread_cond_broadcast(&meeting_cond); // 唤醒所有等待的员工
pthread_mutex_unlock(&meeting_lock);
return NULL;
}
int main() {
pthread_t tid_admin, tid_emp[3];
int emp_ids[3] = {1, 2, 3}; // 3个员工
// 初始化条件变量和互斥锁(动态初始化,用默认属性)
pthread_cond_init(&meeting_cond, NULL);
pthread_mutex_init(&meeting_lock, NULL);
// 创建线程:行政开放会议室,员工申请会议室
pthread_create(&tid_admin, NULL, admin, NULL);
for (int i = 0; i < 3; i++) {
pthread_create(&tid_emp[i], NULL, employee, &emp_ids[i]);
}
// 等待所有线程结束
pthread_join(tid_admin, NULL);
for (int i = 0; i < 3; i++) {
pthread_join(tid_emp[i], NULL);
}
// 销毁资源(避免内存泄漏)
pthread_cond_destroy(&meeting_cond);
pthread_mutex_destroy(&meeting_lock);
return 0;
}
3. 编译与运行(文档要求版)
文档指出,多线程程序编译时需添加
-lpthread 以链接线程库,具体命令如下:
bash
// 编译命令
g++ cond_demo.cpp -o cond_demo -lpthread # 编译
./cond_demo # 运行
4. 预期效果
plaintext
员工1:申请会议室,当前空闲:0间
员工1:没会议室,排队等...
员工2:申请会议室,当前空闲:0间
员工2:没会议室,排队等...
员工3:申请会议室,当前空闲:0间
员工3:没会议室,排队等...
行政:开放3间会议室!
员工3:被通知,检查会议室...
员工3:占用会议室,剩余空闲:2间
员工1:被通知,检查会议室...
员工1:占用会议室,剩余空闲:1间
员工2:被通知,检查会议室...
员工2:占用会议室,剩余空闲:0间
员工3:会议结束,释放会议室
员工3:释放后空闲:1间,通知下一个人
员工1:会议结束,释放会议室
员工1:释放后空闲:2间,通知下一个人
员工2:会议结束,释放会议室
员工2:释放后空闲:3间,通知下一个人
常见陷阱及解决方法——4个初学者易犯的错误(含解决方案)
陷阱1:不使用互斥锁(最严重!)
错误代码示例:
cpp
// 错误代码
pthread_cond_wait(&meeting_cond, NULL); // 第二个参数填NULL,没传互斥锁
后果:多个线程同时进入等待队列或检查条件,导致
free_rooms 计数混乱(如空闲数变为负值),引发竞争条件。
解决方法:
pthread_cond_wait 的第二个参数必须传递互斥锁的地址,确保两者紧密结合。
陷阱2:使用 if
判断条件,而非 while
(虚假唤醒)
ifwhile错误代码示例:
cpp
// 错误代码
if (free_rooms == 0) { // 只检查一次,被唤醒后直接执行
pthread_cond_wait(&meeting_cond, &meeting_lock);
}
后果:可能发生“虚假唤醒”(因系统错误或其他线程误操作),例如员工被唤醒后发现会议室已被他人占用,但仍然执行“占用”操作,造成数据错误。
解决方法:始终使用
while 循环判断条件,确保被唤醒后重新验证条件确实已满足。
陷阱3:唤醒后忘记释放互斥锁(死锁)
错误代码示例:
cpp
// 错误代码
pthread_cond_signal(&meeting_cond);
// 忘记解锁,互斥锁一直被占用
// pthread_mutex_unlock(&meeting_lock);
后果:所有线程卡在
pthread_mutex_lock,无法继续执行,导致程序卡死。
解决方法:锁定和解锁操作必须成对出现,在
signal / broadcast 后务必释放互斥锁。
陷阱4:混淆 signal
和 broadcast
(唤醒错误数量的线程)
signalbroadcast错误场景:仅有一间会议室空闲,但使用
pthread_cond_broadcast 唤醒了所有三个员工,导致三个员工同时争夺一间会议室,引起数据混乱。
解决方法:对于单一资源释放,应使用
signal(唤醒一个线程);对于多个资源释放,应使用 broadcast(唤醒所有线程)。
总结
条件变量的关键在于“让线程有条件地等待和唤醒”,配合互斥锁可以有效解决“忙等”和“竞争条件”问题。牢记以下四点口诀:
- 条件变量 + 互斥锁,同步操作必须成对进行;
- 等待使用
,唤醒时不要盲目猜测,根据需求选择while
/signal
;broadcast - 加锁和解锁必须成对,这样才能远离死锁;
- 销毁资源不可遗忘,以免发生内存泄漏。


雷达卡


京公网安备 11010802022788号







