C++latch一次性线程同步计数

2026-04-11 18:15:29 1363阅读 0评论

C++20 latch:一次性的“红绿灯”,让线程等得明白、放得干脆

写过多线程代码的人,大概都踩过这样的坑:主线程急着要结果,子线程还在初始化;或者十几个工作线程各自忙活,你却不知道它们啥时候真干完了——于是祭出 join()condition_variable、甚至手搓原子计数器……结果逻辑越补越厚,bug 越修越隐晦。

C++20 引入的 std::latch,就是来替你拆掉这些“临时脚手架”的。它不搞循环等待,不支持重用,也不掺和条件判断——它只做一件事:数够了,就永远亮绿灯

这恰恰是它最踏实的地方:一次性、无状态、零歧义。不是“可能完成”,而是“确认全部抵达”。


latch 的核心就一个整数 count,初始化时定死,之后只能减,不能增,也不能重置。调用 arrive() 减一,调用 wait() 阻塞直到归零。一旦归零,所有后续 wait() 立刻返回,arrive() 也依然合法(但不再影响状态)——它不拒绝迟到者,只保证早到者不白等

举个真实场景:启动一个服务模块,需并行加载配置、连接数据库、预热缓存三件事。你不需要它们按顺序完成,只关心“三件全齐了,我才对外提供服务”。

#include <latch>
#include <thread>
#include <vector>

void start_service() {
    std::latch ready(3);  // 等三件事

    std::vector<std::thread> workers;
    workers.emplace_back([&ready] {
        load_config();     // 耗时不定
        ready.arrive();    // 告诉:我好了
    });
    workers.emplace_back([&ready] {
        connect_db();      // 可能超时重试
        ready.arrive();
    });
    workers.emplace_back([&ready] {
        warm_cache();      // 内存密集型
        ready.arrive();
    });

    ready.wait();  // 主线程卡这儿,直到三件事全到位
    std::cout << "✅ 服务已就绪\n";

    for (auto& t : workers) t.join();
}

注意:arrive() 不要求在独立线程里调用。你完全可以在某个回调函数、异步完成处理中调用它——只要逻辑上“这件事算完成了”,就 arrive()。这种松耦合,比死守 join() 的线程生命周期灵活得多。


有人会问:那 barriersemaphore 不也能数数?区别在哪?

barrier 是循环用的——比如多轮迭代计算,每轮都等全员到达再进下一轮。latch 不循环,它只认“第一次归零”。你不能指望它第二次拦住谁

semaphore 更通用,但语义太宽:它管“资源可用数”,可增可减,还能超发。而 latch 的语义是纯粹的“同步点”——它不表达资源,只表达“里程碑是否达成”。这种语义窄化,反而降低了误用概率。

再打个比方:latch 像高铁站的检票闸机,只设一次“发车倒计时”,乘客陆续刷脸通过,倒计时归零,闸机永久开启;barrier 像健身房的团课签到,每节课都重置;semaphore 则像停车场的空位显示器——数字会变,还可能被抢光。


实际编码中,最容易翻车的是生命周期管理。latch 对象必须活过所有 arrive()wait() 调用。常见错误是把它声明在局部作用域,而 arrive() 在分离线程里调用:

// ❌ 危险!ready 可能在子线程调用 arrive 前就被析构
void bad_example() {
    std::latch ready(1);
    std::thread t([&ready] { 
        do_work();
        ready.arrive();  // 此时 ready 可能已销毁!
    });
    t.detach();  // 更糟:彻底失控
}

正确做法只有两个:要么确保 latch 的生存期覆盖全部操作(如上文 start_service 中定义在函数开头),要么用 std::shared_ptr<std::latch> 跨作用域传递。别图省事往 lambda 捕获列表里传引用——除非你百分百确定生命周期。


最后说个实用技巧:latch 天然适合做“异步任务聚合器”的底座。比如你发了 5 个 HTTP 请求,每个回调里调用 arrive(),主线程 wait() 后统一检查结果。它不关心谁快谁慢,只忠实地记录“有多少个已经落地”

没有 notify_one/notify_all 的纠结,没有 mutex 锁粒度的权衡,也没有 future 那种“一个 promise 对应一个 future”的绑定负担。它轻、直、狠——就像拧紧最后一颗螺丝,咔哒一声,整台机器才真正开始运转。

C++ 的并发工具链里,latch 不是最炫的,但可能是最让人安心的。它不许诺更多,只兑现它名字里的那个字:Latch —— 扣上,就不再松开

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,1363人围观)

还没有评论,来说两句吧...

目录[+]