c++20的std::barrier和std::latch如何用于线程同步? (多线程协作)

9次阅读

该用 std::latch 时用于一次性同步,如主线程等待所有工作线程完成;该用 std::barrier 时用于多轮循环同步,如并行迭代中每轮等待所有线程到达。

c++20 的 std::barrier 和 std::latch 如何用于线程同步?(多线程协作)

std::barrierstd::latch 是 C++20 引入的轻量级同步原语,专为“等待一组线程到达某个点”而设计。它们比 std::mutex + std::condition_variable 更简洁、更高效,且无所有权语义(不绑定线程),适合一次性或循环式协作场景。

什么时候该用 std::latch

std::latch 是一次性计数器:初始化后只能递减(count_down())和等待(wait()),不可重置、不可重复使用。典型用于“主线程等所有工作线程完成”这类单次汇聚场景。

  • 常见错误:在 latch 已触发(count == 0)后再次调用 count_down() —— 行为未定义;调用 wait() 后再调用 count_down() 也无效
  • 使用场景:std::thread 启动多个任务,主线程用 latch.wait() 阻塞直到全部结束;或作为 std::jthread 的启动栅栏(配合 std::stop_token 初始化)
  • 性能影响:无锁 实现(通常基于原子操作),开销远低于条件变量;但不能复用,反复创建 / 销毁有分配成本
std::latch done(3); std::thread t1([&]{/* work */ done.count_down(); }); std::thread t2([&]{/* work */ done.count_down(); }); std::thread t3([&]{/* work */ done.count_down(); }); 

done.wait(); // 主线程阻塞,直到三次 count_down 完成 t1.join(); t2.join(); t3.join();

什么时候该用 std::barrier

std::barrier 是可重用的同步点,每次所有参与线程调用 arrive()(或 arrive_and_wait())后自动重置计数,进入下一轮。适合多阶段并行计算,比如迭代算法中的每轮同步。

  • 常见错误:混用 arrive()arrive_and_wait() —— 若部分线程只调用 arrive(),其余线程调用 arrive_and_wait(),则可能死锁(前者不阻塞,后者会等全部到达)
  • 参数差异:std::barrier 构造时可传入 回调函数std::barrier b(n, []{/* once per phase */});),该回调在每轮计数归零后、重置前由 ** 恰好一个 ** 到达线程执行(常用于更新共享状态或检查终止条件)
  • 兼容性注意:MSVC 19.30+、GCC 11+、Clang 12+ 支持;旧版本需手动回退到 std::condition_variable
std::barrier sync(4); std::vector workers; for (int i = 0; i < 4; ++i) {workers.emplace_back([&sync]{for (int round = 0; round < 3; ++round) {// 每轮独立计算             do_work(round);             sync.arrive_and_wait(); // 等其他 3 个线程也到达}     }); } for (auto& t : workers) t.join();

为什么 不用 std::condition_variable 替代?

不是不能,而是容易出错且冗余。用条件变量模拟 latch 需要手动管理互斥量、计数器、通知逻辑;模拟 barrier 还得处理重入、唤醒丢失、虚假唤醒等问题。

立即学习C++ 免费学习笔记(深入)”;

  • 易踩的坑:notify_one() 写成 notify_all() 可能引发惊群;忘记在 wait() 前加 while 循环检查条件;互斥量粒度太粗导致串行化严重
  • 性能差异:条件变量涉及内核态切换,std::barrier/std::latch 在多数情况下纯用户态完成(尤其计数未达阈值时仅原子操作)
  • 语义清晰性:latch.wait() 就是“等全部完成”,barrier.arrive_and_wait() 就是“我到了,大家一起进下一轮”——意图直白,不易误用

实际协作中容易忽略的细节

两者都要求所有参与线程 ** 严格调用相同次数 ** 的同步操作,否则要么永远等待,要么未定义行为。没有运行时校验,编译器也不会报错。

  • std::latch 的初始计数值必须等于预期调用 count_down() 的总次数;少一次 → 永不触发;多一次 → UB
  • std::barrier 的参与线程数在构造时固定,运行时增减线程需额外协调(例如用 std::shared_ptr<:barrier> 并配合引用计数)
  • 异常安全:若线程在到达前抛异常,未调用 arrive()count_down(),整个同步将卡死——必须确保这些调用在 try/catch 或 RAII 包装器中完成

text=ZqhQzanResources