C++20的std::barrier和std::latch如何用于线程同步?

来源:AI教程网作者:北京网站建设头衔:草根站长
导读:本期聚焦于北京网站建设创作的《C++20的std::barrier和std::latch如何用于线程同步?》,敬请观看详情。线程同步中有两个名字相似但语义完全不同的原语容易混淆:std::latch 和 std::barrier。前者是只能向下计数的一次性门闩,计数到达零后所有等待线程被放行,此后无法重置复用;后者是支持多轮集结的可重用屏障,每完成一轮同步就会执行一次可选的完成函数。实际开发中,latch 适合作为启动门或等待一批任务一次性收尾,barrier 更适合把并行任务划分为多个阶段,让线程在阶段边界反复对齐。使用 barrier 时还要注意完成函数不能抛出异常,并且它会在参与线程之一上执行,不能假设是主线程。理解两者在计数方向、重置能力、执行语义上的差异,是避免线程同步死锁和逻辑错误的关键。

多个工作线程完成各自初始化后,如何在主线程继续之前精确等待这些一次性事件?反过来,如果一组线程要反复执行三个阶段,每次都必须全体到齐才能进入下一阶段,又该用什么机制?C++20 标准库给出的答案是 std::latch 与 std::barrier。这两个同步原语都用来协调线程到达点,但生命周期和计数行为截然不同:latch 像一道一次性的门闩,barrier 则像多轮关卡。本文从 API 用法、代码实例、选型对比和常见陷阱几个角度展开,帮助你在多线程协作中选对工具。

C++20的std::barrier和std::latch如何用于线程同步?

std::latch:一次性倒计时门闩

std::latch 定义在头文件 <latch> 中。它的核心是一个可倒计时但不能增加的计数器。构造时需要给出初始计数,典型值是参与并发任务的线程数量。线程完成自己的工作后调用 count_down 将计数减一;调用 wait 的线程会一直阻塞,直到计数变为零。计数到达零后,所有等待中的线程同时被唤醒,并且 latch 不会回到非零状态,所以它天然只适合表达一次性的事件集合。

这个设计解决了一个非常常见的场景:主线程需要等若干个子线程全部就绪后才统一放行,或者所有子线程需要等主线程完成初始化后再开始执行。放行与收尾可以使用同一个 latch,也可以分别使用两个 latch。比如启动阶段主线程持有一个 start_latch,子线程执行到读取共享配置前先 wait;主线程完成配置加载后 count_down,所有子线程就同时醒来。反过来,主线程等所有子任务结束后继续,可以用一个 done_latch,子线程任务末尾执行 count_down,主线程调用 wait。

下面是一个两个 latch 配合的启动门示例:

#include <latch>
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

int main() {
    const int n = 4;
    std::latch ready(1);          // 启动门,由主线程打开
    std::latch done(n);           // 收尾门,等待 n 个子线程

    std::vector<std::thread> workers;
    for (int i = 0; i < n; ++i) {
        workers.emplace_back([i, &ready, &done] {
            ready.wait();         // 等待主线程信号
            std::cout << "worker " << i << " running\n";
            done.count_down();    // 报告自己完成
        });
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    ready.count_down();           // 打开启动门
    done.wait();                  // 主线程等待全部完成
    std::cout << "all workers done\n";

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

latch 还提供 arrive_and_wait,它等价于先执行一次 count_down(1) 再调用 wait。当某个线程既是任务的执行者又是等待者时,这个接口可以减少漏写 count_down 或 wait 的概率。此外还有 try_wait,它会立即返回计数是否已经到零,不会阻塞,适合轮询或不能无条件阻塞的线程。

需要注意的是,latch 的计数不允许小于零。如果在计数已经为零后继续调用 count_down,行为是未定义的,标准不保证能安全地恢复或报错。所以使用 latch 时,团队成员数量和 count_down 次数必须严格匹配。对于需要多次汇聚的场景,应该切换到 std::barrier。

std::barrier:多轮同步与完成函数

std::barrier 定义在头文件 <barrier> 中。和 latch 一样,它内部维护一个预期到达数,但 barrier 在一次同步完成后会自动恢复到初始计数,从而允许同一组线程反复使用。创建 barrier 时通常传入线程数量,以及一个可选的完成函数。这个完成函数在每一轮所有线程都到达后执行,并且只由其中一个到达线程调用。完成函数执行完成后,阻塞中的线程才会被放行进入下一阶段。

barrier 最关键的接口是 arrive_and_wait。它把“我到了”和“等其他人”组合在一起:当前线程将内部计数减一,然后阻塞,直到本轮所有参与者都完成同样的调用。参与线程数量固定时,每一轮调用都会把计数从预期值减到零,再恢复为预期值。对于需要动态退出某个线程的长期任务,可以使用 arrive_and_drop。它会让当前线程离开屏障,并永久减少后续轮次的预期到达数,这样其余线程不必再等待已经离开的参与者。

下面是一个三阶段流水线示例,四个线程在每个阶段同步一次,阶段完成后由完成函数输出提示:

#include <barrier>
#include <iostream>
#include <thread>
#include <vector>
#include <string>

int main() {
    const int n = 4;
    std::barrier phase_sync(n, [] {
        std::cout << "phase complete\n";
    });

    std::vector<std::thread> workers;
    for (int i = 0; i < n; ++i) {
        workers.emplace_back([i, &phase_sync] {
            for (int phase = 1; phase <= 3; ++phase) {
                std::cout << "thread " << i
                          << " processing phase " << phase << "\n";
                phase_sync.arrive_and_wait();  // 每阶段末对齐
            }
        });
    }

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

这段代码的输出顺序会因调度而变化,但可以确定的是:所有线程都会先完成第一阶段输出,然后完成函数打印 phase complete,接着才会进入第二阶段。barrier 把并行的执行流切成明显的阶段边界,非常适合图像处理、物理模拟、并行排序中“计算、交换、再计算”的迭代模式。

完成函数有一个重要约束:它不能抛出异常。如果完成函数抛出异常,程序会调用 std::terminate。完成函数也不应该做的事情包括:再次调用同一个 barrier 的等待接口,这几乎必然导致死锁;修改所有参与线程都依赖的数据时,要清楚它运行在未知线程上,必须自行保证可见性和互斥。通常完成函数只做汇总、递增轮次、输出进度等轻量操作。

如何选择:latch 与 barrier 的边界

从生命周期上看,latch 是单向的,barrier 是循环的。如果问题描述中存在“只等一次”或“只发一次开始信号”,优先 latch;如果问题描述里有“每一轮”“每个阶段”“下一轮迭代”这类词,优先 barrier。这个判断标准虽然简单,但能过滤掉大部分错误设计。

从参与者角色看,latch 更灵活一些。任意线程都可以调用 count_down,也可以只调用 wait 而不参与倒计数,因此非常适合主线程等待多个工作线程,或者工作线程等待主线程初始化。barrier 的典型用法是所有参与者都执行 arrive_and_wait,主线程如果要等待所有阶段完成,通常也要以参与者身份加入,或者另用其他机制观察阶段完成状态。如果让一个非参与线程去等待 barrier,而所有参与线程都在等它,就很容易形成互相等待。

再考虑计数变化。latch 的计数从初始值单调递减到零后结束,中途虽然可以一次减少多个计数,但不能增加。barrier 在每轮结束自动恢复,还可以用 arrive_and_drop 减少下一轮的预期线程数,这给动态线程池带来了便利。比如某个线程发现自己负责的数据已经处理完毕,就可以 arrive_and_drop 离开,其他线程不需要修改整个屏障对象。

将两者差异归纳如下:

特性std::latchstd::barrier
复用能力一次性多轮可重用
计数变化只能减少每轮恢复,可动态减少预期数
完成函数不支持每轮结束执行一次
等待接口wait / arrive_and_waitarrive_and_wait
典型场景启动门、一次性收尾分阶段并行、迭代计算

表格显示了边界,但实际选择还要结合异常安全和可测试性。latch 语义简单,测试起来直观;barrier 的完成函数和动态计数增加了一些复杂度,如果团队中有成员不理解完成函数在哪个线程执行,可能写出有数据竞争或死锁的代码。因此,能用 latch 解决的场景,不必强行使用 barrier。

常见误区和实现细节

第一种误区是把 latch 当 barrier 用。有人会写出 for 循环里反复等待同一个 latch 的代码,但第一轮计数到零后 latch 已经失效,后续等待会立即返回,完全起不到阶段对齐的作用。第二种误区是忽略完成函数不能抛异常。C++ 标准明确要求 completion function 不抛出,即使内部捕获了所有异常,完成函数之外同步状态也依赖它正常返回。

第三种误区是假设完成函数运行在主线程。完成函数由最后一个到达的参与线程执行,这个线程可能是任意一个工作线程。只有这样才能保证在所有参与者都到达后立即执行阶段边界操作。如果完成函数里需要更新 UI 或访问某个线程专属资源,应该在完成函数中设置标志,再由对应线程检查。

第四种误区涉及 arrive_and_drop。它减少的是后续轮次的预期到达数,而不是当前轮次。当前线程必须仍然完成本轮需要做的工作,再调用 arrive_and_drop 表示本轮到达并离开。另一个线程如果在同一轮中反复调用 barrier,可能造成计数错误;标准要求每个线程每轮只能 arrive 一次。

从内存序角度看,barrier 和 latch 都提供了足够的同步关系。一个线程在 count_down 或 arrive_and_wait 之前写入的数据,在其他线程成功等待返回后可见。具体而言,到达操作带有 release 语义,等待返回带有 acquire 语义。因此不必为了共享数据的可见性再手动添加额外的原子变量或互斥量。但如果完成函数要修改共享数据,仍然需要保证同一时刻只有一个线程执行完成函数,而这正是标准已经提供的保证。

在实践中,如果线程数量很多且阶段很短,barrier 的完成函数可能成为串行瓶颈。例如完成函数里进行复杂的日志输出、全局统计、内存分配等,会拖慢每一轮阶段切换。这时应该尽量把重操作移出完成函数,改成由某个参与线程在阶段内部执行,或者使用无锁结构保存统计数据。latch 没有完成函数,通常开销更低,一次唤醒大量线程时更直接。

还有一点容易忽略:两个原语都不处理线程创建和销毁,它们只负责到达点同步。线程仍然需要由调用方通过 std::thread、线程池或其他执行器来管理。不要期望 barrier 或 latch 会主动结束线程,它们只是协调既有执行流。

C++20std::barrierstd::latch修改时间:2026-10-04 01:48:48

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1004/65338.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。