用std::queue+std::mutex+std::condition_variable实现三段式流水线,需独立同步原语、lambda条件等待、notify_one精准通知、atomic shutdown flag配合哨兵、锁内仅pop/front、解锁后处理、手动管理thread生命周期并兜底异常。

直接用 std::thread 搭三段式流水线就能跑起来,但多数人卡在队列同步和 shutdown 逻辑上——不是死锁就是漏数据,根本原因不是不会写线程,而是没理清「谁该等谁」「谁该通知谁」。
std::queue + std::mutex + std::condition_variable 是唯一可靠组合
别碰 std::deque 或无锁队列(除非你真懂 ABA 问题),std::queue 接口干净、行为确定,配合 std::mutex 和 std::condition_variable 能覆盖所有边界场景。
- 每个队列必须配独立的
std::mutex和std::condition_variable:共用会导致误唤醒,比如 stage2 等待时,stage1 的notify_one()可能意外触发 stage3 -
std::condition_variable::wait()必须用 lambda 判断条件:因为 spurious wakeup 存在,只靠empty()返回值判断会崩溃;正确写法是cv.wait(lk, [&]{ return !q.empty() || shutdown_flag.load(); }); - 所有
push()后必须紧接notify_one(),且只通知下游线程:stage1 → notify stage2,stage2 → notify stage3,绝不能跨阶段 notify
shutdown_flag 必须是 std::atomic 且每轮循环检查
靠「队列为空」退出是错的——stage2 可能刚 pop 完最后一条,stage1 还没 push 哨兵,此时 stage2 就提前退出,漏处理。
- 声明为
std::atomic<bool> shutdown_flag{false};</bool>,所有线程在 while 循环开头读取它 - 停止时先置
shutdown_flag.store(true),再对每个输入队列 push 一个哨兵(如空指针或特殊标记值),确保下游能收到终止信号 - wait 条件里必须同时检查
!q.empty()和shutdown_flag.load(),否则可能永远阻塞
pop 和 front 必须在同一个锁区内完成
常见崩溃点:先 if (!q.empty()),再 q.front() —— 中间被其他线程 pop,front() 就访问了空队列。
- 正确模式只有一种:
std::unique_lock<:mutex> lk(mtx); cv.wait(lk, ...); auto item = std::move(q.front()); q.pop();</:mutex> - pop 后立刻 unlock,再做耗时处理(如 decode / compute),避免阻塞上游 push
- 不要在锁内调用可能阻塞或抛异常的函数,比如
process(item)必须在 unlock 之后执行
三阶段线程启动与生命周期管理
不用 std::jthread 或 std::async,它们把 join/stop 封装得太深,掩盖了资源释放顺序问题。
- 三个
std::thread对象用 vector 存,构造时传入 lambda 或可调用对象,捕获各自队列和 flag 引用 - 主线程 stop 流程:置
shutdown_flag→ 对每个输入队列 push 哨兵 → 调用每个 thread 的join() - join 顺序无关紧要,但必须确保所有线程都已退出,才能析构队列和 mutex,否则可能访问已销毁对象
最容易被忽略的是:每个阶段的处理函数(比如 process())如果抛异常,必须 catch 住,否则整个线程终止,流水线就断了——这不是设计缺陷,是现实里 IO 失败、内存不足、解码错误的常态。别指望“不抛异常”,得主动兜底。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











