C++ 多线程与并发系统取向(四)—— std::condition_variable:线程协作与生产者消费者模型(类比 Java wait/notify)

一、先问一个真实工程问题

你有一个任务队列:

  • 生产者线程往队列里放任务
  • 消费者线程从队列里取任务

当队列空时,消费者怎么办?

错误做法 1:忙等

cpp 复制代码
while (queue.empty()) {
    // 什么也不做
}

问题:

  • CPU 100%
  • 浪费资源
  • 多线程系统直接爆炸

这叫:Busy Wait(忙等)

二、正确思路:线程应该"睡眠"

当队列为空:

线程应该休眠,直到有数据。

这就是:

cpp 复制代码
std::condition_variable

三、Java 类比:wait / notify

Java 写法:

java 复制代码
synchronized (lock) {
    while (queue.isEmpty()) {
        lock.wait();
    }
}

生产者:

java 复制代码
synchronized (lock) {
    queue.add(task);
    lock.notify();
}

C++ 的 condition_variable 就是这个机制。

四、condition_variable 的三件套

必须一起使用:

cpp 复制代码
std::mutex
std::unique_lock
std::condition_variable

注意:

必须是 unique_lock,不能是 lock_guard

因为等待时会:

1️⃣ 自动释放锁

2️⃣ 线程睡眠

3️⃣ 被唤醒后重新加锁

五、最小示例(错误写法)

cpp 复制代码
std::unique_lock<std::mutex> lock(mtx);

while (queue.empty()) {
    cv.wait(lock);
}

你可能觉得没问题。

但这里隐藏两个重大风险:

⚠ 风险 1:假唤醒(Spurious Wakeup)

线程可能:

在没有 notify 的情况下醒来。

如果你写成:

cpp 复制代码
if (queue.empty()) {
    cv.wait(lock);
}

那就危险了。

正确写法必须是:

cpp 复制代码
while (queue.empty()) {
    cv.wait(lock);
}

或者:

cpp 复制代码
cv.wait(lock, []{ return !queue.empty(); });

⚠ 风险 2:丢通知

如果:

生产者先 notify

消费者还没进入 wait

那通知就丢了。

正确姿势:

状态必须由 mutex 保护

等待必须基于状态判断

六、正确模型:BlockingQueue 最小闭环

我们写一个工程级可用的阻塞队列。

1️⃣ 类定义

cpp 复制代码
#include <queue>
#include <mutex>
#include <condition_variable>

template<typename T>
class BlockingQueue {
public:
    void push(const T& value) {
        std::unique_lock<std::mutex> lock(mtx_);
        queue_.push(value);
        lock.unlock();          // 解锁后通知(推荐顺序)
        cv_.notify_one();
    }

    T pop() {
        std::unique_lock<std::mutex> lock(mtx_);

        cv_.wait(lock, [this] {
            return !queue_.empty();
        });

        T value = queue_.front();
        queue_.pop();
        return value;
    }

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

2️⃣ 使用示例

cpp 复制代码
#include <thread>
#include <iostream>

BlockingQueue<int> queue;

void producer() {
    for (int i = 0; i < 5; ++i) {
        queue.push(i);
        std::cout << "Produced: " << i << std::endl;
    }
}

void consumer() {
    for (int i = 0; i < 5; ++i) {
        int value = queue.pop();
        std::cout << "Consumed: " << value << std::endl;
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();
}

七、notify_one vs notify_all

方法 作用
notify_one 唤醒一个线程
notify_all 唤醒所有线程

推荐:

  • 单消费者 → notify_one
  • 多消费者 → notify_all(视场景)

八、工程级关键原则(必须记住)

1️⃣ 等待必须带 predicate

永远使用:

cpp 复制代码
cv.wait(lock, condition);

不要自己写 while + wait。

2️⃣ 状态修改必须在锁内

错误:

cpp 复制代码
queue.push(value);
cv.notify_one();

正确:

cpp 复制代码
std::unique_lock lock(mtx);
queue.push(value);
lock.unlock();
cv.notify_one();

3️⃣ 锁保护状态,cv 只负责"通知"

condition_variable 不保护数据。

它只是:

睡眠机制 + 唤醒机制

九、系统取向思维升级

现在你的并发体系已经三层:

第一层:线程模型

共享什么?

第二层:资源保护

mutex + RAII

第三层:线程协作

condition_variable

十、和 Java 再对比

Java C++
synchronized mutex + RAII
wait cv.wait
notify cv.notify_one
notifyAll cv.notify_all

区别:

C++ 更底层,更灵活,也更容易写错。

十一、本篇总结口诀

有共享,用 mutex

要等待,用 cv

等待带条件

状态在锁内

十二、下一篇预告

第五篇我们进入:

std::atomic ------ 原子操作与状态一致性

  • atomic 和 mutex 到底怎么分工?
  • 为什么 atomic 不等于无锁系统?
  • 内存可见性问题
  • 停止标志(stop flag)工程示例
相关推荐
精彩极了吧2 小时前
C++基础知识-(②)面向对象(上)
c++·类和对象·封装·this指针·类的默认成员函数·赋值运算符重载
三水彡彡彡彡2 小时前
深入理解指针:常量、函数与数组
c++·学习
你好!蒋韦杰-(烟雨平生)2 小时前
Opengl模拟水面
c++·游戏·3d
Rhystt2 小时前
代码随想录第二十六天|669. 修剪二叉搜索树、108.将有序数组转换为二叉搜索树、538.把二叉搜索树转换为累加树
数据结构·c++·算法·leetcode
csbysj20202 小时前
Java Override/Overload
开发语言
globaldomain2 小时前
立海世纪:优质品牌域名对企业的潜在价值
开发语言·php·主机·网站·域名注册
wangbing11252 小时前
开发指南142-类和字符串转换
java·开发语言
岱宗夫up2 小时前
【前端基础】HTML + CSS + JavaScript 进阶(一)
开发语言·前端·javascript·css·html
xyq20242 小时前
Shell echo命令详解
开发语言