欢迎光临
我们一直在努力

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

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

  • 原子操作:释放互斥锁,同时将线程加入条件变量等待队列,进入休眠;
  • 线程让出 CPU 时间片,等待被唤醒;
  • 触发唤醒:其他线程发送信号 | 发生虚假唤醒;
  • 被唤醒后,线程重新争抢互斥锁;
  • 只有拿到互斥锁之后,pthread_cond_wait才返回,代码继续向下执行。
  • 重点:函数返回,仅仅代表线程被唤醒并且拿到锁,不代表业务条件已经成立。

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

    业务需求:

  • 生产者线程持续对data自增生产数据;
  • 当data达到 100,通知消费者线程执行消费;
  • 消费者消费完成后,通知生产者继续生产。
  • 用到两个条件变量:

    • cond_produce:生产者等待条件,数据满 100 时生产者休眠;
    • cond_consume:消费者等待条件,数据不足 100 时消费者休眠。

    完整示例代码:

    #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 版本代码片段

    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 将该行为合法化,把条件二次校验的责任交给上层应用代码。

    标准编码范式

    等待方(等待条件满足)

    pthread_mutex_lock(&mutex);
    while (/*条件不满足*/)
    {
    pthread_cond_wait(&cond, &mutex);
    }
    //操作共享资源
    pthread_mutex_unlock(&mutex);

    唤醒方(修改条件、发出通知)

    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 标准编码规范。
  • 生产者‑消费者队列、线程池任务队列,全部遵循这套编码范式。
  • 赞(0)
    未经允许不得转载:171主机测评 » 条件变量(pthread_cond_t):为什么必须用 while,不能用 if
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址