5、条件变量(Condition Variable):std::condition_variable的使用、生产者-消费者模型、虚假唤醒与处理

条件变量,说白了就是线程间的「信号灯」。

你想想看,如果两个线程要协作干活——一个等数据,一个生产数据。轮询检查?太浪费CPU了。直接阻塞等待?又不知道什么时候该醒。这时候就需要条件变量登场了。

我个人习惯把条件变量理解成「门卫」。线程A说:「数据好了叫我一声」,然后就去睡觉。线程B生产完数据,喊一声:「醒醒,活来了!」。嗯,就是这么个意思。

5.1 std::condition_variable 的核心用法

C++11 标准库提供了 std::condition_variable,它必须配合 std::unique_lock<std::mutex> 使用。为什么不能用 lock_guard?因为条件变量需要能够主动解锁和重新加锁,unique_lock 提供了这个灵活性。

核心API就三个:

  • wait(lock, predicate) —— 等条件满足,不满足就睡觉
  • notify_one() —— 叫醒一个等待的线程
  • notify_all() —— 叫醒所有等待的线程

重要提醒: wait() 的第二个参数是个可调用对象(通常是lambda),返回true表示条件已满足。这个参数不是可选的——我建议永远都带上它。

来看一个最基础的例子:

#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });  // 等ready变成true
    std::cout << "工作线程被唤醒了\n";
}

void notifier() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();  // 通知等待的线程
}

int main() {
    std::thread t1(worker);
    std::thread t2(notifier);
    t1.join();
    t2.join();
    return 0;
}

这段代码里,worker 线程会一直阻塞在 wait 上,直到 notifier 线程把 ready 设为 true 并调用 notify_one()。注意那个大括号——我习惯把修改共享状态和通知分开,先释放锁再通知,能减少一些不必要的竞争。

5.2 生产者-消费者模型

这是条件变量最经典的应用场景。我在项目中遇到过好几次,从日志系统到任务队列,本质上都是这个模型。

核心思路:

  • 生产者往队列里放数据,放完后通知消费者
  • 消费者从队列里取数据,队列为空就等着
  • 队列本身需要互斥锁保护
#include <queue>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>

template<typename T>
class ThreadSafeQueue {
public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(mtx_);
        queue_.push(std::move(value));
        cv_.notify_one();  // 通知一个消费者
    }

    T pop() {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_.wait(lock, [this]{ return !queue_.empty(); });
        T value = std::move(queue_.front());
        queue_.pop();
        return value;
    }

private:
    std::queue<T> queue_;
    std::mutex mtx_;
    std::condition_variable cv_;
};

// 使用示例
ThreadSafeQueue<int> queue;

void producer() {
    for (int i = 0; i < 10; ++i) {
        queue.push(i);
        std::cout << "生产: " << i << "\n";
    }
}

void consumer() {
    for (int i = 0; i < 10; ++i) {
        int value = queue.pop();
        std::cout << "消费: " << value << "\n";
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

我的经验: 生产者和消费者的数量不一定非要1:1。多个生产者、多个消费者同时工作也很常见。这时候 notify_one()notify_all() 的选择就很重要了——如果多个消费者等待不同类型的数据,用 notify_all() 可能更合适。

5.3 虚假唤醒(Spurious Wakeup)

嗯,这里要注意。虚假唤醒是个坑,而且是个「系统级」的坑。

什么叫虚假唤醒?就是你的线程明明没收到 notify,却自己醒过来了。为什么会这样?说白了,操作系统在实现条件变量时,为了性能和正确性,允许线程在某些情况下「提前」返回。这不是bug,是特性。

我曾经在一个网络框架里踩过这个坑。当时写了个简单的 wait 没加条件判断,结果在高负载下偶尔会出现消费者取到空数据的情况。排查了半天,最后发现是虚假唤醒导致的。

避坑指南: 永远不要在 wait() 之后假设条件一定满足。必须用while循环或者带谓词的 wait 来重新检查条件。

正确的写法有两种:

// 方式一:while循环(C++11之前的标准做法)
std::unique_lock<std::mutex> lock(mtx);
while (!condition) {
    cv.wait(lock);
}

// 方式二:带谓词的wait(推荐)
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return condition; });

方式二其实就是方式一的语法糖。底层实现里,wait 内部会帮你做while循环检查。所以我个人强烈建议:永远使用带谓词的 wait。省事,安全,不容易忘。

5.4 知识体系图

下面这张图总结了条件变量的核心知识点和它们之间的关系:

条件变量知识体系 std::condition_variable wait(lock, predicate) notify_one() notify_all() 生产者-消费者模型 线程池任务队列 事件驱动/通知机制 ⚠ 虚假唤醒(Spurious Wakeup) while循环检查条件 带谓词的 wait()

5.5 几个实用建议

最后,分享几个我在实际项目中积累的经验:

  1. 锁和条件变量要一一对应。 不要用一个条件变量等待多个不同的条件,容易搞混。每个逻辑条件配一个条件变量,清晰明了。
  2. notify 之前先解锁。 虽然 notify 本身不要求锁,但如果在持有锁的情况下通知,被唤醒的线程会立即尝试获取锁,可能造成不必要的上下文切换。我习惯先解锁再通知。
  3. 小心死锁。 如果多个条件变量嵌套等待,很容易死锁。我一般会避免在一个等待里再去等待另一个条件变量。
  4. 考虑使用 std::condition_variable_any 它跟 std::condition_variable 功能一样,但可以配合任何满足 BasicLockable 要求的锁,比如 std::shared_lock。不过性能略差,一般场景用不到。

一个小技巧: 调试条件变量相关的问题时,可以在 wait 前后加日志,记录线程ID和时间戳。我曾经靠这个快速定位了一个「消费者永远等不到数据」的bug——原来是生产者忘记调用 notify 了。

条件变量用好了,线程间的协作会非常优雅。用不好,就是各种诡异的挂起和超时。记住核心原则:永远检查条件,永远不要假设唤醒就是正确的信号。做到这一点,大部分坑你都能绕过去。


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