Linux 条件变量:线程同步的利器
条件变量(Condition Variable)是 POSIX 线程(pthread)库中最重要、最常用的线程间同步原语之一。它与互斥锁(mutex)配合使用,主要解决“等待某个条件成立”的场景,是实现生产者-消费者模型、线程池任务等待、状态变更通知等经典模式的首选工具。
一、条件变量的核心思想
条件变量的核心语义只有一句话:
“某个线程可以挂起(等待),直到另一个线程通知它‘条件可能已经满足了’”
它不保存任何状态,只负责唤醒和等待。
条件变量必须和互斥锁一起使用,这是铁律。
最经典的使用模式(伪代码):
pthread_mutex_t mutex;
pthread_cond_t cond;
// 等待条件成立
pthread_mutex_lock(&mutex);
while (条件不满足) {
pthread_cond_wait(&cond, &mutex); // 释放锁 → 等待 → 被唤醒后重新加锁
}
... // 条件满足,处理业务
pthread_mutex_unlock(&mutex);
// 通知等待者
pthread_mutex_lock(&mutex);
... // 修改条件
pthread_cond_signal(&cond); // 唤醒一个
// 或 pthread_cond_broadcast(&cond); // 唤醒全部
pthread_mutex_unlock(&mutex);
二、条件变量的核心 API(全家桶)
| 函数 | 作用 | 是否阻塞 | 必须持有锁? | 典型使用场景 |
|---|---|---|---|---|
pthread_cond_init | 初始化条件变量 | 否 | 否 | 创建时调用 |
pthread_cond_destroy | 销毁条件变量 | 否 | 否 | 不再使用时 |
pthread_cond_wait | 等待条件变量被通知(释放锁) | 是 | 是 | 核心等待函数 |
pthread_cond_timedwait | 带超时的等待 | 是 | 是 | 需要超时保护的等待 |
pthread_cond_signal | 唤醒至少一个等待线程 | 否 | 建议持有 | 单消费者或无差别唤醒 |
pthread_cond_broadcast | 唤醒所有等待线程 | 否 | 建议持有 | 多消费者、状态广播 |
三、最经典的正确写法(必须掌握)
1. 生产者-消费者模型(单生产者多消费者)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_SIZE 10
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;
int buffer[BUFFER_SIZE];
int count = 0;
int in = 0, out = 0;
void* producer(void* arg) {
for (int i = 0; i < 20; i++) {
pthread_mutex_lock(&mutex);
// 等待不满
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_not_full, &mutex);
}
buffer[in] = i;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("生产: %d (count=%d)\n", i, count);
pthread_cond_broadcast(&cond_not_empty); // 通知消费者
pthread_mutex_unlock(&mutex);
usleep(rand() % 100000);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 20; i++) {
pthread_mutex_lock(&mutex);
// 等待不空
while (count == 0) {
pthread_cond_wait(&cond_not_empty, &mutex);
}
int data = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("消费: %d (count=%d)\n", data, count);
pthread_cond_signal(&cond_not_full); // 通知生产者
pthread_mutex_unlock(&mutex);
usleep(rand() % 200000);
}
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;
}
2. 为什么 while 而不是 if?(虚假唤醒)
虚假唤醒(spurious wakeup) 是条件变量的天然特性:
- 即使没人 signal,线程也可能被系统唤醒
- 被唤醒后条件不一定成立
因此必须用 while 循环检查条件,这是 POSIX 标准强烈推荐的写法:
while (条件不满足) {
pthread_cond_wait(&cond, &mutex);
}
永远不要用 if!
四、条件变量常见误区与正确姿势
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 用 if 而不是 while 判断条件 | 虚假唤醒导致逻辑错误 | 永远用 while |
| 在 signal/broadcast 后不解锁 | 可能导致被唤醒线程立即再次阻塞 | 先 signal,再 unlock(或一起在锁内) |
| 忘记初始化条件变量 | 未定义行为 | 总是 pthread_cond_init |
| 在不持有锁的情况下 signal | 可能丢失唤醒 | 建议在持有锁时 signal |
| 使用同一个 cond 变量做多种等待 | 唤醒错乱 | 每种等待条件用独立的 cond |
| 忘记销毁 cond 和 mutex | 资源泄漏 | 程序结束前 destroy |
五、进阶技巧与工程实践
- 带超时的等待(防止永久阻塞)
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5; // 超时 5 秒
pthread_cond_timedwait(&cond, &mutex, &ts);
- 多条件等待(经典写法)
pthread_mutex_lock(&mutex);
while (!ready || !data_available) {
if (!ready) pthread_cond_wait(&cond_ready, &mutex);
if (!data_available) pthread_cond_wait(&cond_data, &mutex);
}
- 条件变量 + 互斥锁的最佳实践命名
pthread_mutex_t queue_mutex;
pthread_cond_t queue_not_empty;
pthread_cond_t queue_not_full;
- 线程池中任务等待的典型用法
// 线程池 worker 线程
while (1) {
pthread_mutex_lock(&pool->mutex);
while (queue_empty(pool->task_queue)) {
pthread_cond_wait(&pool->cond, &pool->mutex);
}
task = dequeue(pool->task_queue);
pthread_mutex_unlock(&pool->mutex);
task->func(task->arg);
}
六、总结:一句话记住条件变量
条件变量不是用来“保存条件”的,而是用来“通知条件可能变了”的。
它必须搭配互斥锁使用,等待时用 while + cond_wait,通知时用 signal 或 broadcast。
掌握了条件变量 + 互斥锁,你就真正掌握了线程同步的精髓,几乎所有复杂的多线程协作模式(线程池、任务队列、状态机、缓存失效通知、barrier 等)都可以基于它构建。
如果你想看更复杂的例子(比如多生产者多消费者、带优先级的条件等待、实现线程安全的队列、条件变量在实际项目中的封装),可以直接告诉我,我可以继续给出完整代码和详细说明。