线程同步之条件变量:让线程学会“等待”和“通知”
各位同学,今天我们来聊聊多线程编程中一个非常实用的工具——条件变量。说实话,我刚接触多线程那会儿,总觉得互斥锁就能解决所有同步问题。直到有一次,我写一个生产者-消费者程序,发现消费者线程一直在空转轮询,CPU占用率飙到100%……嗯,那时候我才意识到,光有互斥锁是不够的。
条件变量的作用:解决“什么时候该干活”的问题
条件变量,说白了就是让线程能够等待某个条件成立,或者通知其他线程条件已经满足。
你想想看,如果只用互斥锁,线程只能保证“同一时间只有一个人访问共享资源”。但很多时候,我们需要的是“等数据准备好了再处理”。比如:
- 消费者线程:等队列里有数据了,我才去取
- 生产者线程:等队列没满,我才往里放
没有条件变量的话,你只能让消费者线程不停地检查队列状态——这就是所谓的忙等待。我在一个网络服务器项目中就踩过这个坑,CPU白白浪费在轮询上,吞吐量直接腰斩。
核心思想:条件变量让线程在条件不满足时阻塞休眠,而不是空转。条件满足时,由其他线程唤醒它。这样既省CPU,又及时响应。
pthread_cond_t 的使用:四个函数搞定
在POSIX线程库中,条件变量用 pthread_cond_t 类型表示。我个人习惯把它和互斥锁配对使用,因为条件变量的等待和唤醒操作,都需要在锁的保护下进行。
常用的四个函数:
| 函数 | 作用 |
|---|---|
pthread_cond_init |
初始化条件变量 |
pthread_cond_wait |
等待条件成立(会释放锁,被唤醒后重新获取锁) |
pthread_cond_signal |
唤醒一个等待该条件的线程 |
pthread_cond_broadcast |
唤醒所有等待该条件的线程 |
pthread_cond_destroy |
销毁条件变量 |
这里有个关键点,我当年学的时候绕了好久——pthread_cond_wait 到底干了什么?
它实际上做了三件事:
- 释放互斥锁(让其他线程能访问共享资源)
- 阻塞等待条件变量被唤醒
- 重新获取互斥锁(被唤醒后,继续往下执行前先拿锁)
嗯,这里要注意:pthread_cond_wait 返回时,并不代表条件一定成立了!它只是告诉你“有人通知我了,你去检查一下条件吧”。所以,我们通常把它放在一个 while 循环里。
重要:永远不要用 if 来判断条件,要用 while!因为可能存在虚假唤醒——线程被唤醒但条件并未满足。我在一个金融交易系统中就遇到过,用 if 导致消费者取到了空数据,程序直接崩溃。
生产者消费者模型:经典案例实战
好了,理论说完了,咱们直接上代码。这个模型我几乎在每个多线程项目里都用过,比如日志系统、任务队列、网络缓冲区……
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0; // 当前队列中的产品数量
int in = 0, out = 0; // 生产位置和消费位置
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER;
// 生产者线程
void* producer(void* arg) {
int id = *(int*)arg;
int item = 0;
while (1) {
pthread_mutex_lock(&mutex);
// 队列满了,等待消费者消费
while (count == BUFFER_SIZE) {
printf("生产者 %d: 队列满了,等待...\n", id);
pthread_cond_wait(&cond_producer, &mutex);
}
// 生产一个产品
item = rand() % 100;
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("生产者 %d: 生产了 %d,当前数量 %d\n", id, item, count);
// 通知消费者:有新产品了
pthread_cond_signal(&cond_consumer);
pthread_mutex_unlock(&mutex);
sleep(1); // 模拟生产耗时
}
return NULL;
}
// 消费者线程
void* consumer(void* arg) {
int id = *(int*)arg;
int item;
while (1) {
pthread_mutex_lock(&mutex);
// 队列空了,等待生产者生产
while (count == 0) {
printf("消费者 %d: 队列空了,等待...\n", id);
pthread_cond_wait(&cond_consumer, &mutex);
}
// 消费一个产品
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("消费者 %d: 消费了 %d,当前数量 %d\n", id, item, count);
// 通知生产者:有空位了
pthread_cond_signal(&cond_producer);
pthread_mutex_unlock(&mutex);
sleep(2); // 模拟消费耗时
}
return NULL;
}
int main() {
pthread_t producers[2], consumers[2];
int ids[4] = {1, 2, 3, 4};
// 创建2个生产者线程和2个消费者线程
pthread_create(&producers[0], NULL, producer, &ids[0]);
pthread_create(&producers[1], NULL, producer, &ids[1]);
pthread_create(&consumers[0], NULL, consumer, &ids[2]);
pthread_create(&consumers[1], NULL, consumer, &ids[3]);
// 等待线程结束(这里不会结束,按Ctrl+C终止)
pthread_join(producers[0], NULL);
pthread_join(producers[1], NULL);
pthread_join(consumers[0], NULL);
pthread_join(consumers[1], NULL);
return 0;
}
个人经验:我建议在条件变量的 while 循环里加个打印日志,方便调试。曾经有一次,我的消费者线程一直卡在等待状态,就是因为生产者忘记调用 pthread_cond_signal 了。加个日志一眼就能看出来。
核心逻辑流程图
下面我用一张图来展示生产者-消费者模型的核心流程,方便你理解线程之间的协作关系:
避坑指南:我踩过的那些坑
讲到这里,我想分享几个实战中容易犯的错误:
- 忘记在 while 循环里检查条件:我曾经用
if替代while,结果在高并发下,消费者被唤醒后队列又被其他线程清空了,直接读到脏数据。这个bug查了我整整两天。 - signal 和 broadcast 选错:如果只有一个线程在等待,用
signal就够了。如果有多个线程等待不同条件,用broadcast更安全。我一般倾向于用broadcast,虽然性能稍差,但不容易漏唤醒。 - 忘记解锁:在
pthread_cond_wait之前一定要确保已经持有锁,否则会报错。这个错误编译时不会报,运行时才崩溃,很坑。
总结一下:条件变量是解决“等待-通知”问题的利器。记住三个要点:
- 用
while循环检查条件 - 条件变量必须和互斥锁配合使用
- 生产/消费完成后及时
signal或broadcast
掌握了这些,你就能写出高效、健壮的多线程程序了。
可可精品资料库