线程同步之条件变量:条件变量的原理、pthread_cond_t的使用、生产者-消费者模型、虚假唤醒问题

说到线程同步,很多人第一反应就是互斥锁。但我在实际项目中遇到过一个问题:一个线程要等某个条件成立才能继续执行,如果用互斥锁轮询检查,CPU占用率直接飙到100%。这显然不是个好办法。条件变量就是专门解决这类问题的。

条件变量的基本原理

条件变量,说白了就是让线程在某个条件不满足时「睡大觉」,等条件满足了再被叫醒。它本身不提供互斥功能,必须配合互斥锁一起使用。

你想想看,如果让线程一直循环检查某个标志位,那得多浪费CPU?条件变量的核心思想就是:让线程阻塞等待,而不是忙等待

核心要点:条件变量允许一个线程阻塞自己,直到另一个线程通知它条件已经满足。这比轮询检查效率高得多。

pthread_cond_t 的使用

在POSIX线程库中,条件变量的类型是 pthread_cond_t。使用前需要初始化,用完后需要销毁。

初始化和销毁

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  // 静态初始化

// 或者动态初始化
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);

// 销毁
pthread_cond_destroy(&cond);

等待和通知

条件变量有三个核心操作:

函数 作用
pthread_cond_wait() 阻塞等待条件成立,同时释放互斥锁
pthread_cond_signal() 唤醒一个等待该条件的线程
pthread_cond_broadcast() 唤醒所有等待该条件的线程

这里有个关键点:pthread_cond_wait() 会原子性地完成两个动作——释放互斥锁并进入等待状态。当线程被唤醒时,它会重新获取互斥锁。这个设计非常巧妙,避免了竞态条件。

个人习惯:我每次用条件变量,都会在 pthread_cond_wait() 外面套一个 while 循环。为什么?后面讲虚假唤醒时你就明白了。

生产者-消费者模型

生产者-消费者模型是条件变量最经典的应用场景。我当年刚学多线程时,就是靠这个例子彻底搞懂了条件变量的用法。

来看一个完整的例子:

#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;     // 生产者写入位置
int out = 0;    // 消费者读取位置

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER;

void* producer(void* arg) {
    int item = 0;
    while (1) {
        pthread_mutex_lock(&mutex);
        
        // 缓冲区满了,生产者等待
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_producer, &mutex);
        }
        
        // 生产一个物品
        buffer[in] = item++;
        in = (in + 1) % BUFFER_SIZE;
        count++;
        
        printf("生产者生产了物品 %d,当前数量: %d\n", buffer[(in - 1 + BUFFER_SIZE) % BUFFER_SIZE], count);
        
        // 通知消费者
        pthread_cond_signal(&cond_consumer);
        
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return NULL;
}

void* consumer(void* arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        
        // 缓冲区空了,消费者等待
        while (count == 0) {
            pthread_cond_wait(&cond_consumer, &mutex);
        }
        
        // 消费一个物品
        int item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        count--;
        
        printf("消费者消费了物品 %d,当前数量: %d\n", item, count);
        
        // 通知生产者
        pthread_cond_signal(&cond_producer);
        
        pthread_mutex_unlock(&mutex);
        sleep(2);
    }
    return NULL;
}

int main() {
    pthread_t prod_tid, cons_tid;
    
    pthread_create(&prod_tid, NULL, producer, NULL);
    pthread_create(&cons_tid, NULL, consumer, NULL);
    
    pthread_join(prod_tid, NULL);
    pthread_join(cons_tid, NULL);
    
    return 0;
}

这个模型里,生产者和消费者各自有自己的条件变量。生产者等「缓冲区不满」,消费者等「缓冲区不空」。互斥锁保护共享数据 count 和缓冲区。

注意:千万不要在 pthread_cond_wait() 之前忘记加锁,也不要在 pthread_cond_signal() 之后忘记解锁。我曾经见过一个同事把锁放错了位置,结果程序跑起来直接死锁,排查了半天。

虚假唤醒问题

虚假唤醒,听起来有点玄乎。但我在项目中确实遇到过。什么意思呢?就是线程明明没有被 pthread_cond_signal()pthread_cond_broadcast() 通知,却自己从 pthread_cond_wait() 返回了。

为什么会这样?

嗯,这其实是操作系统层面的行为。有些系统为了性能优化,可能会让等待的线程偶尔醒来检查一下条件。POSIX标准明确说了:pthread_cond_wait() 可能被虚假唤醒,这是允许的。

那怎么解决?

很简单:pthread_cond_wait() 放在 while 循环里,而不是 if 语句里

看这段代码:

// 错误写法:用 if
if (count == 0) {
    pthread_cond_wait(&cond_consumer, &mutex);
}

// 正确写法:用 while
while (count == 0) {
    pthread_cond_wait(&cond_consumer, &mutex);
}

用 while 循环的好处是:即使线程被虚假唤醒,它也会重新检查条件。如果条件不满足,就继续等待。这样虚假唤醒就不会造成任何问题。

避坑指南:我曾经在一个网络服务器项目里,因为用了 if 而不是 while,导致消费者线程被虚假唤醒后读取了空缓冲区,程序直接崩溃。从那以后,我写条件变量必用 while 循环,已经成了肌肉记忆。

条件变量的完整使用模式

总结一下,条件变量的标准使用模式就三步:

  1. 加锁:获取互斥锁
  2. 等待条件:在 while 循环中调用 pthread_cond_wait()
  3. 处理数据:条件满足后执行操作
  4. 解锁:释放互斥锁

通知端也类似:

  1. 加锁:获取互斥锁
  2. 修改条件:改变共享数据
  3. 发送通知:调用 pthread_cond_signal()pthread_cond_broadcast()
  4. 解锁:释放互斥锁

小技巧:如果你不确定用 signal 还是 broadcast,记住一条原则:如果所有等待线程都能处理同一个条件变化,用 broadcast;如果只需要唤醒一个线程来处理,用 signal。用错了最多浪费点性能,不会出大问题。

条件变量的核心流程图

下面这张图展示了条件变量的完整工作流程,我建议你仔细看看:

条件变量工作流程 生产者线程 消费者线程 共享缓冲区 (受互斥锁保护) 条件变量 1. 加锁 2. 检查条件(while循环) 1. 加锁 2. 检查条件(while循环) 条件不满足 → 线程阻塞在 pthread_cond_wait() 条件满足 → 线程被唤醒,重新获取锁 虚假唤醒 → while循环重新检查条件 生产者 消费者 条件变量

从这张图可以看出,生产者和消费者都通过互斥锁访问共享缓冲区。当条件不满足时(比如缓冲区满或空),线程会通过条件变量阻塞等待。条件变量在这里起到了「协调员」的作用,让线程在合适的时机被唤醒。

总结

条件变量是多线程编程中不可或缺的同步工具。它的核心价值在于:让线程在条件不满足时休眠,避免浪费CPU资源

使用条件变量时,记住三个要点:

  • 必须配合互斥锁使用,不能单独使用
  • 用 while 循环检查条件,防止虚假唤醒
  • 先修改条件,再发送通知,顺序不能搞反

我在多个项目中都用条件变量实现了高效的生产者-消费者模型,包括日志系统、网络消息队列、任务调度器等。只要掌握了这个工具,很多并发问题都能迎刃而解。


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