名词区分
条件变量
pthread_cond_t:运行期线程同步工具,搭配普通 C 语言while/if(不带#),用于线程等待某个事件发生。条件编译
#if / #endif:编译期预处理指令,带#,仅做代码裁剪,和线程运行逻辑无关。
前置概念
1. 临界资源、临界区
- 临界资源 :多个线程都可以读写的共享数据,本文示例为全局变量
data。 - 临界区:操作临界资源的代码片段。
多个线程同时执行临界区,会篡改破坏共享数据,需要互斥锁做保护。
2. 互斥锁 pthread_mutex_t
作用:保证同一时刻,最多只有一个线程执行临界区代码。
pthread_mutex_lock:加锁pthread_mutex_unlock:解锁
条件变量不能脱离互斥锁单独使用。条件变量负责让线程休眠等待;互斥锁用来保护被判断的共享数据。
++3. 虚假唤醒++
POSIX 标准许可的底层现象:线程已经休眠、挂在条件变量等待队列上,没有其他线程发送signal唤醒信号,内核却把休眠的线程唤醒。
这不是代码 Bug,普通测试很难复现,高并发压力场景才容易暴露。
4. pthread_cond_wait 执行流程
调用前提:调用该函数之前,线程必须已经持有互斥锁。
- 原子操作:释放互斥锁,同时将线程加入条件变量等待队列,进入休眠;
- 线程让出 CPU 时间片,等待被唤醒;
- 触发唤醒:其他线程发送信号 | 发生虚假唤醒;
- 被唤醒后,线程重新争抢互斥锁;
- 只有拿到互斥锁之后,
pthread_cond_wait才返回,代码继续向下执行。
重点:函数返回,仅仅代表线程被唤醒并且拿到锁,不代表业务条件已经成立。
业务场景:生产者‑消费者
业务需求:
- 生产者线程持续对
data自增生产数据; - 当
data达到 100,通知消费者线程执行消费; - 消费者消费完成后,通知生产者继续生产。
用到两个条件变量:
cond_produce:生产者等待条件,数据满 100 时生产者休眠;cond_consume:消费者等待条件,数据不足 100 时消费者休眠。
完整示例代码:
cs
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_produce = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consume = PTHREAD_COND_INITIALIZER;
int data = 0;
//消费者线程:等待data达到100再执行消费
void *consumer(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
while(data < 100)
{
pthread_cond_wait(&cond_consume, &mutex);
}
printf("【消费者】开始消费,data=%d\n", data);
data = 0;
printf("【消费者】消费完成,data=%d\n", data);
pthread_cond_signal(&cond_produce);
pthread_mutex_unlock(&mutex);
usleep(100000);
}
return NULL;
}
//生产者线程:累加data,满100就等待消费完成
void *producer(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
data++;
printf("【生产者】生产,data=%d\n", data);
if(data >= 100)
{
printf("【生产者】数据已满,通知消费者\n");
pthread_cond_signal(&cond_consume);
while(data >= 100)
{
pthread_cond_wait(&cond_produce, &mutex);
}
}
pthread_mutex_unlock(&mutex);
usleep(100000);
}
return NULL;
}
int main(void)
{
pthread_t tid_pro, tid_con;
pthread_create(&tid_pro, NULL, producer, NULL);
pthread_create(&tid_con, NULL, consumer, NULL);
pthread_join(tid_pro, NULL);
pthread_join(tid_con, NULL);
return 0;
}
重点讲解:为什么不能使用 if
❌错误 if 版本代码片段
pthread_mutex_lock(&mutex);
if(data < 100) // 条件只会判断一次
{
pthread_cond_wait(&cond_consume, &mutex);
}
data = 0;
pthread_mutex_unlock(&mutex);
if 版本时序(虚假唤醒场景)
- 初始状态
data = 0; - 消费者拿到锁,
if(data < 100)条件成立,进入 if 大括号; - 调用
pthread_cond_wait:内部原子解锁,线程休眠,挂在条件变量等待队列。
✅此时线程停在
pthread_cond_wait函数内部,函数还没有返回。👉虚假唤醒就发生在这里 :没有任何线程调用
signal,操作系统内核直接唤醒这个休眠的线程。
- 线程被唤醒,内部竞争拿到互斥锁,
pthread_cond_wait返回; if仅在进入大括号之前做一次判断;函数返回后,不会再次校验条件;- 直接执行
data = 0,此时data依旧为 0,业务逻辑错乱。
✅正确 while 版本代码片段
cs
pthread_mutex_lock(&mutex);
while(data < 100) // 每一次wait返回,都会重新校验条件
{
pthread_cond_wait(&cond_consume, &mutex);
}
data = 0;
pthread_mutex_unlock(&mutex);
while 版本对应时序
data=0,满足while(data<100),进入循环,调用pthread_cond_wait,线程休眠;- 发生虚假唤醒,线程被内核唤醒、抢到锁,
pthread_cond_wait返回; - 执行流回到while 循环头部 ,重新执行条件判断
data < 100; - 判断发现条件依旧成立,再次调用
pthread_cond_wait继续休眠等待。
核心结论:
pthread_cond_wait返回 ≠ 业务条件成立。
signal 与 broadcast
pthread_cond_signal:唤醒等待队列中至少一个线程。pthread_cond_broadcast:唤醒全部正在等待的线程。
即便使用
signal,也必须写while。signal 仅负责唤醒线程,无法规避虚假唤醒。 补充:虚假唤醒不属于内核 Bug。为简化内核内部实现,POSIX 将该行为合法化,把条件二次校验的责任交给上层应用代码。
标准编码范式
等待方(等待条件满足)
cs
pthread_mutex_lock(&mutex);
while (/*条件不满足*/)
{
pthread_cond_wait(&cond, &mutex);
}
//操作共享资源
pthread_mutex_unlock(&mutex);
唤醒方(修改条件、发出通知)
cs
pthread_mutex_lock(&mutex);
//修改共享数据
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
开发注意事项
- 即使程序只有一个等待线程,也不能改用
if。虚假唤醒和线程数量无关,统一使用while。 while带来的判断开销很小。只有线程被唤醒才执行一次判断;条件不满足线程直接休眠,不会空转消耗 CPU。- 条件变量必须搭配互斥锁。共享数据需要锁保护,同时
pthread_cond_wait接口也要求传入互斥锁。 - 禁止用
#if替代while。#if属于编译期处理,无法读取程序运行时变量。
总结
- 调用
pthread_cond_wait前线程必须持有锁;函数内部原子解锁后休眠;被唤醒后竞争锁,拿到锁函数才返回。 - 虚假唤醒发生在线程已经休眠于
pthread_cond_wait内部的时候;函数返回不能代表业务条件满足。 - 使用条件变量做等待,必须用
while循环校验条件,禁止if,这是 POSIX 标准编码规范。 - 生产者‑消费者队列、线程池任务队列,全部遵循这套编码范式。