线程同步之条件变量:条件变量的原理、pthread_cond_t的使用、生产者-消费者模型、虚假唤醒问题
说到线程同步,很多人第一反应就是互斥锁。但我在实际项目中遇到过一个问题:一个线程要等某个条件成立才能继续执行,如果用互斥锁轮询检查,CPU占用率直接飙到100%。这显然不是个好办法。条件变量就是专门解决这类问题的。
条件变量的基本原理
条件变量,说白了就是让线程在某个条件不满足时「睡大觉」,等条件满足了再被叫醒。它本身不提供互斥功能,必须配合互斥锁一起使用。
你想想看,如果让线程一直循环检查某个标志位,那得多浪费CPU?条件变量的核心思想就是:让线程阻塞等待,而不是忙等待。
核心要点:条件变量允许一个线程阻塞自己,直到另一个线程通知它条件已经满足。这比轮询检查效率高得多。
pthread_cond_t 的使用
在POSIX线程库中,条件变量的类型是 pthread_cond_t。使用前需要初始化,用完后需要销毁。
初始化和销毁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 静态初始化
// 或者动态初始化
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
// 销毁
pthread_cond_destroy(&cond);
等待和通知
条件变量有三个核心操作:
| 函数 | 作用 |
|---|---|
pthread_cond_wait() |
阻塞等待条件成立,同时释放互斥锁 |
pthread_cond_signal() |
唤醒一个等待该条件的线程 |
pthread_cond_broadcast() |
唤醒所有等待该条件的线程 |
这里有个关键点:pthread_cond_wait() 会原子性地完成两个动作——释放互斥锁并进入等待状态。当线程被唤醒时,它会重新获取互斥锁。这个设计非常巧妙,避免了竞态条件。
个人习惯:我每次用条件变量,都会在 pthread_cond_wait() 外面套一个 while 循环。为什么?后面讲虚假唤醒时你就明白了。
生产者-消费者模型
生产者-消费者模型是条件变量最经典的应用场景。我当年刚学多线程时,就是靠这个例子彻底搞懂了条件变量的用法。
来看一个完整的例子:
#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; // 生产者写入位置
int 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 item = 0;
while (1) {
pthread_mutex_lock(&mutex);
// 缓冲区满了,生产者等待
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_producer, &mutex);
}
// 生产一个物品
buffer[in] = item++;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("生产者生产了物品 %d,当前数量: %d\n", buffer[(in - 1 + BUFFER_SIZE) % BUFFER_SIZE], count);
// 通知消费者
pthread_cond_signal(&cond_consumer);
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
// 缓冲区空了,消费者等待
while (count == 0) {
pthread_cond_wait(&cond_consumer, &mutex);
}
// 消费一个物品
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("消费者消费了物品 %d,当前数量: %d\n", item, count);
// 通知生产者
pthread_cond_signal(&cond_producer);
pthread_mutex_unlock(&mutex);
sleep(2);
}
return NULL;
}
int main() {
pthread_t prod_tid, cons_tid;
pthread_create(&prod_tid, NULL, producer, NULL);
pthread_create(&cons_tid, NULL, consumer, NULL);
pthread_join(prod_tid, NULL);
pthread_join(cons_tid, NULL);
return 0;
}
这个模型里,生产者和消费者各自有自己的条件变量。生产者等「缓冲区不满」,消费者等「缓冲区不空」。互斥锁保护共享数据 count 和缓冲区。
注意:千万不要在 pthread_cond_wait() 之前忘记加锁,也不要在 pthread_cond_signal() 之后忘记解锁。我曾经见过一个同事把锁放错了位置,结果程序跑起来直接死锁,排查了半天。
虚假唤醒问题
虚假唤醒,听起来有点玄乎。但我在项目中确实遇到过。什么意思呢?就是线程明明没有被 pthread_cond_signal() 或 pthread_cond_broadcast() 通知,却自己从 pthread_cond_wait() 返回了。
为什么会这样?
嗯,这其实是操作系统层面的行为。有些系统为了性能优化,可能会让等待的线程偶尔醒来检查一下条件。POSIX标准明确说了:pthread_cond_wait() 可能被虚假唤醒,这是允许的。
那怎么解决?
很简单:把 pthread_cond_wait() 放在 while 循环里,而不是 if 语句里。
看这段代码:
// 错误写法:用 if
if (count == 0) {
pthread_cond_wait(&cond_consumer, &mutex);
}
// 正确写法:用 while
while (count == 0) {
pthread_cond_wait(&cond_consumer, &mutex);
}
用 while 循环的好处是:即使线程被虚假唤醒,它也会重新检查条件。如果条件不满足,就继续等待。这样虚假唤醒就不会造成任何问题。
避坑指南:我曾经在一个网络服务器项目里,因为用了 if 而不是 while,导致消费者线程被虚假唤醒后读取了空缓冲区,程序直接崩溃。从那以后,我写条件变量必用 while 循环,已经成了肌肉记忆。
条件变量的完整使用模式
总结一下,条件变量的标准使用模式就三步:
- 加锁:获取互斥锁
- 等待条件:在 while 循环中调用
pthread_cond_wait() - 处理数据:条件满足后执行操作
- 解锁:释放互斥锁
通知端也类似:
- 加锁:获取互斥锁
- 修改条件:改变共享数据
- 发送通知:调用
pthread_cond_signal()或pthread_cond_broadcast() - 解锁:释放互斥锁
小技巧:如果你不确定用 signal 还是 broadcast,记住一条原则:如果所有等待线程都能处理同一个条件变化,用 broadcast;如果只需要唤醒一个线程来处理,用 signal。用错了最多浪费点性能,不会出大问题。
条件变量的核心流程图
下面这张图展示了条件变量的完整工作流程,我建议你仔细看看:
从这张图可以看出,生产者和消费者都通过互斥锁访问共享缓冲区。当条件不满足时(比如缓冲区满或空),线程会通过条件变量阻塞等待。条件变量在这里起到了「协调员」的作用,让线程在合适的时机被唤醒。
总结
条件变量是多线程编程中不可或缺的同步工具。它的核心价值在于:让线程在条件不满足时休眠,避免浪费CPU资源。
使用条件变量时,记住三个要点:
- 必须配合互斥锁使用,不能单独使用
- 用 while 循环检查条件,防止虚假唤醒
- 先修改条件,再发送通知,顺序不能搞反
我在多个项目中都用条件变量实现了高效的生产者-消费者模型,包括日志系统、网络消息队列、任务调度器等。只要掌握了这个工具,很多并发问题都能迎刃而解。
公众号:蓝海资料掘金营,微信deep3321