
用 std::thread 搭建三段式流水线最简结构
核心就一句话:每个阶段一个 std::thread,用 std::queue + std::mutex + std::condition_variable 串起来。别一开始就上 std::jthread 或 std::async,它们掩盖了唤醒、阻塞和边界判断的真实逻辑。
典型三段:生产(stage1)→ 处理(stage2)→ 汇总(stage3)。每个线程只关心自己的输入队列和输出队列,不越界读写。
实操建议:
- 每个队列单独配一对
std::mutex和std::condition_variable,别共用——否则 stage2 等待数据时可能误唤醒 stage3 - 所有
push操作后必须调用notify_one(),但只通知「下游」线程,比如 stage1push到 queue1 后 notify stage2,不 notify stage3 - 用
std::queue而非std::deque即可,除非你需要中间随机访问;std::queue的接口更清晰,误操作风险低 - 停止信号统一用
std::atomic<bool></bool>全局控制,所有线程在每次循环开头检查它,而不是靠队列为空来判断退出
避免 std::queue 竞态的加锁模式
常见错误是只锁 push 不锁 pop,或反过来;更隐蔽的是在 empty() 和 front() 之间没加锁,导致 front() 崩溃。
立即学习“C++免费学习笔记(深入)”;
正确模式只有一种:每次访问前加锁,访问完立刻解锁,且 empty() 和 pop() 必须在同一个锁区内完成。
示例(stage2 主循环节选):
while (!shutdown_flag.load()) {
std::unique_lock<std::mutex> lk(q1_mutex);
q1_cv.wait(lk, [&]{ return !q1.empty() || shutdown_flag.load(); });
if (shutdown_flag.load() && q1.empty()) break;
auto item = std::move(q1.front());
q1.pop();
lk.unlock(); // 解锁后再处理,避免阻塞 stage1
auto result = process(item);
std::lock_guard<std::mutex> lk2(q2_mutex);
q2.push(std::move(result));
q2_cv.notify_one();
}
std::condition_variable::wait 为什么必须用 lambda 判断条件
直接 wait(lk) 是错的,会丢数据或死锁。因为 wait 可能被虚假唤醒(spurious wakeup),此时队列可能仍为空。
lambda 里写的不是“等一次”,而是“等直到满足条件”。它会在每次唤醒后重新求值,确保安全。
容易踩的坑:
- lambda 捕获方式用
[&],别用[=]——shutdown_flag是std::atomic,按值捕获会复制旧值,导致永远等不到退出 - lambda 内不要做耗时操作(如日志、IO),它在持有互斥锁时执行,会拖慢整个流水线
- 别把
shutdown_flag.load()和!q1.empty()写成||就完事;顺序很重要,优先检查 shutdown,避免在已关闭时还尝试 pop
性能瓶颈往往卡在锁粒度和内存拷贝上
流水线吞吐量上不去?八成问题不在算法,而在两个地方:一是每个 item 进出队列都深拷贝,二是所有线程抢同一把锁(哪怕你写了多把,但设计不合理也会退化)。
优化方向很实际:
- 用
std::unique_ptr<t></t>或std::shared_ptr<t></t>在队列里传指针,避免T的拷贝构造;如果T支持移动,确保定义了移动构造函数 - stage2 如果要多次读写同一 item,别在每次
pop后立刻move出来——先front()取引用处理,最后再pop() - 别为了“看起来快”把
std::mutex换成std::shared_mutex:这里全是写操作(push/pop),读写锁毫无优势,反而增加开销 - 如果 stage1 生产极快而 stage3 极慢,考虑给 stage2 输出队列加长度限制(带界队列),配合
wait_for实现背压,否则内存会持续上涨
真正难调的不是启动线程,而是让三个线程节奏咬合——太快会积压,太慢会空转,而边界条件(启动、停止、异常)比主循环更费调试时间。


















