74、条件变量:pthread_cond_wait 的使用
条件变量,说白了就是线程之间的一种「通知机制」。
我刚开始学多线程编程那会儿,总觉得互斥锁就够了——你锁我锁大家锁,天下太平。后来遇到一个实际场景:生产者线程往队列里放数据,消费者线程等着取数据。如果队列空了,消费者总不能一直轮询吧?那CPU得烧起来。
这时候,条件变量就派上用场了。
条件变量的核心思想
条件变量本身不提供「互斥」能力,它只做一件事:让一个线程在某个条件不满足时「睡下去」,等条件满足了再「被叫醒」。
你想想看,这比轮询优雅多了。轮询就像你每隔5秒去厨房看一眼水烧开没有,条件变量就像水壶装了哨子——水开了它叫你。
核心要点:条件变量必须和互斥锁配合使用。这不是可选的,是强制的。
pthread_cond_wait 的函数原型
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这个函数做了三件事,顺序很重要:
- 释放互斥锁——让其他线程能拿到锁
- 阻塞等待——直到条件变量被唤醒
- 重新获取锁——被唤醒后,函数返回前会重新锁上
嗯,这里要注意:pthread_cond_wait 返回时,锁一定是被当前线程持有的。这个设计很巧妙,但也容易踩坑。
标准使用模式
我个人的习惯是,条件变量永远搭配 while 循环使用,而不是 if。为什么?看代码:
// 消费者线程
pthread_mutex_lock(&mutex);
while (queue_is_empty()) {
pthread_cond_wait(&cond, &mutex);
}
// 现在队列非空,可以取数据了
data = dequeue();
pthread_mutex_unlock(&mutex);
为什么用 while 而不是 if?
因为 pthread_cond_wait 可能被「虚假唤醒」。什么叫虚假唤醒?就是条件变量没被 signal,线程自己莫名其妙醒过来了。这在 POSIX 标准里是允许的,虽然不常见,但你不能假设它不会发生。
重要:永远用 while 循环检查条件,不要用 if。这是多线程编程的黄金法则之一。
生产者线程怎么通知?
对应的,生产者线程在放入数据后,需要通知消费者:
// 生产者线程
pthread_mutex_lock(&mutex);
enqueue(data);
pthread_cond_signal(&cond); // 唤醒一个等待的线程
// 或者 pthread_cond_broadcast(&cond); // 唤醒所有等待的线程
pthread_mutex_unlock(&mutex);
这里有个细节:pthread_cond_signal 可以在锁内调用,也可以在锁外调用。我个人习惯在锁内调用,逻辑更清晰。不过有些高性能场景会在解锁后再 signal,能减少一次上下文切换。这个看具体需求。
一个完整的例子
我写一个简单的生产者-消费者模型,你感受一下:
#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_not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_not_empty = PTHREAD_COND_INITIALIZER;
void *producer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
item = rand() % 100;
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_not_full, &mutex);
}
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("生产者: 放入 %d, 当前数量 %d\n", item, count);
pthread_cond_signal(&cond_not_empty);
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
void *consumer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&cond_not_empty, &mutex);
}
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("消费者: 取出 %d, 当前数量 %d\n", item, count);
pthread_cond_signal(&cond_not_full);
pthread_mutex_unlock(&mutex);
sleep(2);
}
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
return 0;
}
这个例子用了两个条件变量:一个表示「缓冲区没满」(生产者可以放),一个表示「缓冲区没空」(消费者可以取)。这样生产者不会在缓冲区满时还傻等,消费者也不会在空时白忙活。
条件变量的核心流程
我把整个逻辑画成了一张图,方便你理解:
避坑指南
我在项目中遇到过不少条件变量相关的 bug,挑几个典型的说说:
- 忘记用 while 循环——这是最常见的坑。我曾经在一个网络库里用了 if,结果在高并发下偶尔出现消费者读到空数据的情况。排查了两天才发现是虚假唤醒导致的。
- signal 和 broadcast 选错——如果多个线程等待的条件不同,用 broadcast 会唤醒所有线程,但只有一个能拿到锁继续执行,其他线程会再次检查条件然后继续等待。这会造成「惊群效应」。我建议:能确定只唤醒一个线程时,用 signal;不确定时,用 broadcast 更安全。
- 在未加锁的情况下调用 cond_wait——这是未定义行为,程序可能直接崩溃。记住:调用 cond_wait 前,当前线程必须持有互斥锁。
- 忘记在条件满足后 signal——生产者放了数据但忘了通知消费者,消费者就一直睡下去了。这种 bug 很难复现,因为有时候生产者放得快,消费者还没睡下去就被处理了。但一旦时序不对,就死锁了。
调试小技巧:如果你怀疑条件变量有问题,可以在 signal 前后加打印日志,记录线程 ID 和当前条件状态。多跑几次,看唤醒和等待是否匹配。
总结一下
条件变量是线程同步的利器,但用起来需要小心。记住三个要点:
- 必须搭配互斥锁使用
- 条件检查用 while 不用 if
- signal 和 broadcast 按需选择
嗯,其实条件变量用熟了之后,你会发现它比信号量、事件等机制更灵活、更高效。多写几个例子,自然就上手了。
公众号:蓝海资料掘金营,微信deep3321