条件变量(pthread_cond_t):为什么必须用 while,不能用 if

名词区分

  • 条件变量 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 执行流程

调用前提:调用该函数之前,线程必须已经持有互斥锁。

  1. 原子操作:释放互斥锁,同时将线程加入条件变量等待队列,进入休眠;
  2. 线程让出 CPU 时间片,等待被唤醒;
  3. 触发唤醒:其他线程发送信号 | 发生虚假唤醒;
  4. 被唤醒后,线程重新争抢互斥锁;
  5. 只有拿到互斥锁之后,pthread_cond_wait才返回,代码继续向下执行。

重点:函数返回,仅仅代表线程被唤醒并且拿到锁,不代表业务条件已经成立。

业务场景:生产者‑消费者

业务需求:

  1. 生产者线程持续对data自增生产数据;
  2. 当data达到 100,通知消费者线程执行消费;
  3. 消费者消费完成后,通知生产者继续生产。

用到两个条件变量:

  • 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 版本时序(虚假唤醒场景)
  1. 初始状态 data = 0;
  2. 消费者拿到锁,if(data < 100)条件成立,进入 if 大括号;
  3. 调用pthread_cond_wait:内部原子解锁,线程休眠,挂在条件变量等待队列。

✅此时线程停在pthread_cond_wait函数内部,函数还没有返回。

👉虚假唤醒就发生在这里 :没有任何线程调用signal,操作系统内核直接唤醒这个休眠的线程。

  1. 线程被唤醒,内部竞争拿到互斥锁,pthread_cond_wait返回;
  2. if仅在进入大括号之前做一次判断;函数返回后,不会再次校验条件;
  3. 直接执行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 版本对应时序
  1. data=0,满足while(data<100),进入循环,调用pthread_cond_wait,线程休眠;
  2. 发生虚假唤醒,线程被内核唤醒、抢到锁,pthread_cond_wait返回;
  3. 执行流回到while 循环头部 ,重新执行条件判断data < 100;
  4. 判断发现条件依旧成立,再次调用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);

开发注意事项

  1. 即使程序只有一个等待线程,也不能改用if。虚假唤醒和线程数量无关,统一使用while。
  2. while带来的判断开销很小。只有线程被唤醒才执行一次判断;条件不满足线程直接休眠,不会空转消耗 CPU。
  3. 条件变量必须搭配互斥锁。共享数据需要锁保护,同时pthread_cond_wait接口也要求传入互斥锁。
  4. 禁止用#if替代while。#if属于编译期处理,无法读取程序运行时变量。

总结

  1. 调用pthread_cond_wait前线程必须持有锁;函数内部原子解锁后休眠;被唤醒后竞争锁,拿到锁函数才返回。
  2. 虚假唤醒发生在线程已经休眠于pthread_cond_wait内部的时候;函数返回不能代表业务条件满足。
  3. 使用条件变量做等待,必须用while循环校验条件,禁止if,这是 POSIX 标准编码规范。
  4. 生产者‑消费者队列、线程池任务队列,全部遵循这套编码范式。
相关推荐
AIgorithmGEEK6 天前
[Linux]线程三部曲(上):一个执行流的诞生——从操作系统一路拆到 pthread_create
linux·线程·pid
流浪0018 天前
Linux系统篇36——线程(一) 线程的概念、本质和Linux的实现方式
linux·面试·操作系统·线程
weixin_4407305014 天前
队列QUEUE介绍+类型示例(先进先出、后进先出、优先级高先出、双端进入、生产者+消费者线程)
python·线程·queue
handler0117 天前
【Linux】线程与虚拟内存的底层世界
linux·运维·服务器·c++·线程·虚拟地址空间·共享内存
LXMXHJ18 天前
springboot中的线程操作
java·spring boot·后端·线程
handler0120 天前
【Linux】虚拟地址空间解析
linux·运维·c++·线程·进程·虚拟地址空间·虚拟地址
神威难绷泪1 个月前
Linux应用软件编程:线程分离属性 互斥机制 同步机制 死锁
linux·开发语言·算法·线程
bryant_meng1 个月前
【Codex】Part 18 — Codex Context Lifecycle Management
chatgpt·线程·压缩·codex·prompt缓存
在世修行1 个月前
多线程取流与线程同步:工业上位机的并发艺术
线程·线程同步·线程锁