用std::mutex和std::condition_variable实现生产者消费者队列时,必须用带谓词的wait循环检测条件、notify前更新状态、避免虚假唤醒;多消费者场景下notify_one更高效,std::queue+锁比无锁队列更稳妥可靠。

用 std::mutex + std::condition_variable 实现基础版生产者消费者
标准库里没有现成的“生产者消费者队列”,得自己搭。核心是保护共享队列、阻塞空消费和满生产,std::mutex 和 std::condition_variable 是唯二必须的同步原语。
常见错误是只锁不等,比如消费者发现队列空就直接 return,结果漏掉后续通知;或者生产者没检查容量就 push,导致死锁或数据丢失。
- 消费者循环里必须用
cv.wait(lock, [&]{ return !queue.empty(); }),不能写成if (queue.empty()) cv.wait(...) - 每次
notify_one()前要确保状态已更新(比如 push 或 pop 完成),否则唤醒后仍可能失败 -
std::condition_variable::wait有虚假唤醒风险,必须配合谓词 lambda 使用,不能裸调用
示例片段:
std::queue<int> queue;
std::mutex mtx;
std::condition_variable cv_produce, cv_consume;
// 生产者
void produce(int val) {
std::unique_lock<std::mutex> lock(mtx);
cv_produce.wait(lock, [&]{ return queue.size() < CAPACITY; });
queue.push(val);
cv_consume.notify_one(); // 通知消费者有新数据
}
// 消费者
int consume() {
std::unique_lock<std::mutex> lock(mtx);
cv_consume.wait(lock, [&]{ return !queue.empty(); });
int val = queue.front();
queue.pop();
cv_produce.notify_one(); // 通知生产者有空位
return val;
}
为什么不用 std::semaphore(C++20)?
C++20 引入了 std::counting_semaphore,理论上更贴近信号量语义,但实际用起来限制多、兼容性差。
立即学习“C++免费学习笔记(深入)”;
主要问题不是功能不够,而是落地难:MSVC 19.3x 才开始支持,Clang 12+ 且需 -std=c++20 -lpthread,GCC 12+ 默认不启用。很多项目还卡在 C++17。
- 即使编译通过,
std::counting_semaphore构造时传负数会抛std::system_error,而传统 condvar 方案出错更早、更容易定位 - 它不提供“等待满足某条件”的能力,比如“队列非空且元素为偶数”,还得套一层 mutex + lambda
- 调试时无法像
std::condition_variable那样观察 wait 的谓词逻辑,堆栈里只看到sem_wait
多个生产者 / 多个消费者时,notify_one() 还够用吗?
够,但要看场景。单个消费者时用 notify_one() 是最优解;多个消费者共用一个队列时,也推荐 notify_one(),除非你明确需要“广播式唤醒”。
误用 notify_all() 是高频性能坑:100 个消费者线程同时被唤醒,只有 1 个能抢到锁并消费,其余 99 个立刻再次进入 wait,白白消耗 CPU 和上下文切换。
- 如果消费者处理耗时长,且希望“尽可能让空闲消费者立即接手”,才考虑
notify_one()配合std::this_thread::yield()让出时间片 - 若存在优先级区分(如高优任务插队),需额外加字段标记,不能只靠 notify 策略解决
- 所有 notify 调用必须在持有 mutex 的前提下完成,否则行为未定义
队列类型选 std::queue 还是 boost::lockfree::queue?
绝大多数情况坚持用 std::queue + 锁。无锁队列不是银弹,反而容易引入更隐蔽的问题。
boost::lockfree::queue 要求元素可复制/可移动,且默认固定大小;一旦 push 失败(队列满),它直接返回 false,不会阻塞——这和生产者消费者的原始语义冲突,你得自己补重试或降级逻辑。
- 内存模型复杂:它依赖
std::atomic底层操作,在 ARM 或旧 x86 上可能因内存序问题导致读到脏数据 - 调试困难:gdb 无法自然打印其内部状态,
size()返回值也不一定实时准确 - 仅当单生产者单消费者(SPSC)且吞吐压测证明瓶颈真在锁上时,才值得评估替换
真正该优化的地方通常是队列操作本身:避免在临界区做深拷贝、预分配空间、用 std::move 转移大对象。








