74、条件变量:pthread_cond_wait 的使用

条件变量,说白了就是线程之间的一种「通知机制」。

我刚开始学多线程编程那会儿,总觉得互斥锁就够了——你锁我锁大家锁,天下太平。后来遇到一个实际场景:生产者线程往队列里放数据,消费者线程等着取数据。如果队列空了,消费者总不能一直轮询吧?那CPU得烧起来。

这时候,条件变量就派上用场了。

条件变量的核心思想

条件变量本身不提供「互斥」能力,它只做一件事:让一个线程在某个条件不满足时「睡下去」,等条件满足了再「被叫醒」。

你想想看,这比轮询优雅多了。轮询就像你每隔5秒去厨房看一眼水烧开没有,条件变量就像水壶装了哨子——水开了它叫你。

核心要点:条件变量必须和互斥锁配合使用。这不是可选的,是强制的。

pthread_cond_wait 的函数原型

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

这个函数做了三件事,顺序很重要:

  1. 释放互斥锁——让其他线程能拿到锁
  2. 阻塞等待——直到条件变量被唤醒
  3. 重新获取锁——被唤醒后,函数返回前会重新锁上

嗯,这里要注意:pthread_cond_wait 返回时,锁一定是被当前线程持有的。这个设计很巧妙,但也容易踩坑。

标准使用模式

我个人的习惯是,条件变量永远搭配 while 循环使用,而不是 if。为什么?看代码:

// 消费者线程
pthread_mutex_lock(&mutex);
while (queue_is_empty()) {
    pthread_cond_wait(&cond, &mutex);
}
// 现在队列非空,可以取数据了
data = dequeue();
pthread_mutex_unlock(&mutex);

为什么用 while 而不是 if?

因为 pthread_cond_wait 可能被「虚假唤醒」。什么叫虚假唤醒?就是条件变量没被 signal,线程自己莫名其妙醒过来了。这在 POSIX 标准里是允许的,虽然不常见,但你不能假设它不会发生。

重要:永远用 while 循环检查条件,不要用 if。这是多线程编程的黄金法则之一。

生产者线程怎么通知?

对应的,生产者线程在放入数据后,需要通知消费者:

// 生产者线程
pthread_mutex_lock(&mutex);
enqueue(data);
pthread_cond_signal(&cond);  // 唤醒一个等待的线程
// 或者 pthread_cond_broadcast(&cond);  // 唤醒所有等待的线程
pthread_mutex_unlock(&mutex);

这里有个细节:pthread_cond_signal 可以在锁内调用,也可以在锁外调用。我个人习惯在锁内调用,逻辑更清晰。不过有些高性能场景会在解锁后再 signal,能减少一次上下文切换。这个看具体需求。

一个完整的例子

我写一个简单的生产者-消费者模型,你感受一下:

#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, out = 0;

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;

void *producer(void *arg) {
    int item;
    for (int i = 0; i < 10; i++) {
        item = rand() % 100;
        pthread_mutex_lock(&mutex);

        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_not_full, &mutex);
        }

        buffer[in] = item;
        in = (in + 1) % BUFFER_SIZE;
        count++;

        printf("生产者: 放入 %d, 当前数量 %d\n", item, count);

        pthread_cond_signal(&cond_not_empty);
        pthread_mutex_unlock(&mutex);

        sleep(1);
    }
    return NULL;
}

void *consumer(void *arg) {
    int item;
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);

        while (count == 0) {
            pthread_cond_wait(&cond_not_empty, &mutex);
        }

        item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        count--;

        printf("消费者: 取出 %d, 当前数量 %d\n", item, count);

        pthread_cond_signal(&cond_not_full);
        pthread_mutex_unlock(&mutex);

        sleep(2);
    }
    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;
}

这个例子用了两个条件变量:一个表示「缓冲区没满」(生产者可以放),一个表示「缓冲区没空」(消费者可以取)。这样生产者不会在缓冲区满时还傻等,消费者也不会在空时白忙活。

条件变量的核心流程

我把整个逻辑画成了一张图,方便你理解:

pthread_cond_wait 执行流程 生产者线程 消费者线程 pthread_mutex_lock 条件满足? pthread_cond_wait 释放锁 → 阻塞 → 重新加锁 signal/broadcast 注意:被唤醒后,pthread_cond_wait 返回前会重新获取互斥锁

避坑指南

我在项目中遇到过不少条件变量相关的 bug,挑几个典型的说说:

  • 忘记用 while 循环——这是最常见的坑。我曾经在一个网络库里用了 if,结果在高并发下偶尔出现消费者读到空数据的情况。排查了两天才发现是虚假唤醒导致的。
  • signal 和 broadcast 选错——如果多个线程等待的条件不同,用 broadcast 会唤醒所有线程,但只有一个能拿到锁继续执行,其他线程会再次检查条件然后继续等待。这会造成「惊群效应」。我建议:能确定只唤醒一个线程时,用 signal;不确定时,用 broadcast 更安全。
  • 在未加锁的情况下调用 cond_wait——这是未定义行为,程序可能直接崩溃。记住:调用 cond_wait 前,当前线程必须持有互斥锁。
  • 忘记在条件满足后 signal——生产者放了数据但忘了通知消费者,消费者就一直睡下去了。这种 bug 很难复现,因为有时候生产者放得快,消费者还没睡下去就被处理了。但一旦时序不对,就死锁了。

调试小技巧:如果你怀疑条件变量有问题,可以在 signal 前后加打印日志,记录线程 ID 和当前条件状态。多跑几次,看唤醒和等待是否匹配。

总结一下

条件变量是线程同步的利器,但用起来需要小心。记住三个要点:

  • 必须搭配互斥锁使用
  • 条件检查用 while 不用 if
  • signal 和 broadcast 按需选择

嗯,其实条件变量用熟了之后,你会发现它比信号量、事件等机制更灵活、更高效。多写几个例子,自然就上手了。


公众号:蓝海资料掘金营,微信deep3321