C++ STL 之 latch / barrier / semaphore 详解(C++20 并发新原语)

C++ STL 之 latch / barrier / semaphore 详解(C++20 并发新原语)

一、为什么需要新的并发原语

C++11 提供了 std::threadstd::mutexstd::condition_variablestd::future,但这些工具在面对"等待一组任务完成"、"限制并发数量"等常见场景时,要么需要手写复杂的同步逻辑,要么性能不够理想。C++20 从 POSIX 和 Java 等生态引入了三个标准库同步原语:

原语 作用 是否可复用
std::latch 一次性同步点,N 个线程到达后放行
std::barrier 可复用同步点,每轮到达触发回调
std::counting_semaphore 计数信号量,控制资源访问数

它们都定义在 <latch><barrier><semaphore> 头文件中,底层通常基于原子操作或 futex 实现,比条件变量方案更轻量。

二、latch------一次性栅栏

std::latch 是一个向下计数的一次性屏障 。构造时指定初始计数,线程调用 count_down() 减少计数,调用 wait() 阻塞直到计数归零。计数到达零后永久保持,不可重置。

cpp 复制代码
#include <latch>
#include <thread>
#include <vector>
#include <iostream>

void demo_latch() {
    std::latch done{3};  // 等待 3 个任务

    std::vector<std::jthread> workers;
    for (int i = 0; i < 3; ++i) {
        workers.emplace_back([&, i] {
            // 模拟工作
            std::this_thread::sleep_for(std::chrono::milliseconds(100 * i));
            std::cout << "worker " << i << " done\n";
            done.count_down();  // 计数减 1
        });
    }

    done.wait();  // 等待所有 3 个 worker
    std::cout << "all workers done, proceed\n";
}

关键 API:

  • count_down(n = 1) ------ 计数减 n,不阻塞
  • wait() ------ 阻塞直到计数为 0
  • arrive_and_wait(n = 1) ------ 等价于 count_down(n) + wait()
  • try_wait() ------ 非阻塞检查计数是否为 0

典型场景:初始化加载、多路并行计算后合并结果、所有线程就绪后同时开始。

三、barrier------可复用栅栏

std::barrier 是 latch 的升级版。它像一个"回合制"同步点:每轮 N 个线程全部到达后,一起放行,然后自动进入下一轮。每轮结束时还会执行一个完成回调(completion function)。

cpp 复制代码
#include <barrier>
#include <thread>
#include <iostream>

void demo_barrier() {
    constexpr int N = 3;
    int round = 0;
    std::barrier sync{N, [&]() noexcept {
        std::cout << "=== round " << ++round << " done ===\n";
    }};

    std::vector<std::jthread> workers;
    for (int i = 0; i < N; ++i) {
        workers.emplace_back([&, i] {
            for (int r = 0; r < 3; ++r) {
                std::this_thread::sleep_for(std::chrono::milliseconds(50 * i));
                std::cout << "worker " << i << " arrives at round " << r << "\n";
                sync.arrive_and_wait();  // 到达并等待本轮所有人
            }
        });
    }
}

每次 arrive_and_wait() 返回时,所有线程都位于同一轮次的同一时间点------这就是 barrier 的核心保证。
#mermaid-svg-TzvfkCMQ8KQGJiYx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TzvfkCMQ8KQGJiYx .error-icon{fill:#552222;}#mermaid-svg-TzvfkCMQ8KQGJiYx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TzvfkCMQ8KQGJiYx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .marker.cross{stroke:#333333;}#mermaid-svg-TzvfkCMQ8KQGJiYx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TzvfkCMQ8KQGJiYx p{margin:0;}#mermaid-svg-TzvfkCMQ8KQGJiYx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .cluster-label text{fill:#333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .cluster-label span{color:#333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .cluster-label span p{background-color:transparent;}#mermaid-svg-TzvfkCMQ8KQGJiYx .label text,#mermaid-svg-TzvfkCMQ8KQGJiYx span{fill:#333;color:#333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .node rect,#mermaid-svg-TzvfkCMQ8KQGJiYx .node circle,#mermaid-svg-TzvfkCMQ8KQGJiYx .node ellipse,#mermaid-svg-TzvfkCMQ8KQGJiYx .node polygon,#mermaid-svg-TzvfkCMQ8KQGJiYx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TzvfkCMQ8KQGJiYx .rough-node .label text,#mermaid-svg-TzvfkCMQ8KQGJiYx .node .label text,#mermaid-svg-TzvfkCMQ8KQGJiYx .image-shape .label,#mermaid-svg-TzvfkCMQ8KQGJiYx .icon-shape .label{text-anchor:middle;}#mermaid-svg-TzvfkCMQ8KQGJiYx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TzvfkCMQ8KQGJiYx .rough-node .label,#mermaid-svg-TzvfkCMQ8KQGJiYx .node .label,#mermaid-svg-TzvfkCMQ8KQGJiYx .image-shape .label,#mermaid-svg-TzvfkCMQ8KQGJiYx .icon-shape .label{text-align:center;}#mermaid-svg-TzvfkCMQ8KQGJiYx .node.clickable{cursor:pointer;}#mermaid-svg-TzvfkCMQ8KQGJiYx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .arrowheadPath{fill:#333333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TzvfkCMQ8KQGJiYx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TzvfkCMQ8KQGJiYx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TzvfkCMQ8KQGJiYx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TzvfkCMQ8KQGJiYx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TzvfkCMQ8KQGJiYx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TzvfkCMQ8KQGJiYx .cluster text{fill:#333;}#mermaid-svg-TzvfkCMQ8KQGJiYx .cluster span{color:#333;}#mermaid-svg-TzvfkCMQ8KQGJiYx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TzvfkCMQ8KQGJiYx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TzvfkCMQ8KQGJiYx rect.text{fill:none;stroke-width:0;}#mermaid-svg-TzvfkCMQ8KQGJiYx .icon-shape,#mermaid-svg-TzvfkCMQ8KQGJiYx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TzvfkCMQ8KQGJiYx .icon-shape p,#mermaid-svg-TzvfkCMQ8KQGJiYx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TzvfkCMQ8KQGJiYx .icon-shape .label rect,#mermaid-svg-TzvfkCMQ8KQGJiYx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TzvfkCMQ8KQGJiYx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TzvfkCMQ8KQGJiYx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TzvfkCMQ8KQGJiYx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程 1 到达
barrier 等待
线程 2 到达
线程 3 到达
执行完成回调
所有线程放行
线程 1 下一轮
线程 2 下一轮
线程 3 下一轮

关键 API 与 latch 的差异:

  • arrive(n = 1) ------ 到达,返回 std::barrier::arrival_token(用于 wait() 分离)
  • arrive_and_wait() ------ 到达并等待
  • arrive_and_drop() ------ 永久减少本轮参与线程数(退出专用)

典型场景:迭代计算中每轮结束后同步、游戏帧循环中的多线程阶段同步、流水线作业中所有阶段对齐。

四、semaphore------计数信号量

std::counting_semaphore<LeastMaxValue> 是一个计数信号量 ,模板参数 LeastMaxValue 指定最大计数的下限。std::binary_semaphore 是其特化 counting_semaphore<1>

cpp 复制代码
#include <semaphore>
#include <thread>
#include <queue>
#include <iostream>

std::counting_semaphore<10> sem{3};   // 最多 3 个并发
std::mutex mtx;
std::queue<int> q;

void producer() {
    for (int i = 0; i < 10; ++i) {
        sem.acquire();                 // P 操作:计数减 1,满则阻塞
        {
            std::lock_guard lk{mtx};
            q.push(i);
            std::cout << "produce " << i << "\n";
        }
    }
}

void consumer() {
    for (int i = 0; i < 10; ++i) {
        int val;
        {
            std::lock_guard lk{mtx};
            if (!q.empty()) {
                val = q.front();
                q.pop();
            }
        }
        sem.release();                 // V 操作:计数加 1,唤醒等待者
        std::cout << "consume " << val << "\n";
    }
}

#mermaid-svg-ZsQdfYWaMzfKvNeA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZsQdfYWaMzfKvNeA .error-icon{fill:#552222;}#mermaid-svg-ZsQdfYWaMzfKvNeA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZsQdfYWaMzfKvNeA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .marker.cross{stroke:#333333;}#mermaid-svg-ZsQdfYWaMzfKvNeA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZsQdfYWaMzfKvNeA p{margin:0;}#mermaid-svg-ZsQdfYWaMzfKvNeA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .cluster-label text{fill:#333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .cluster-label span{color:#333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .cluster-label span p{background-color:transparent;}#mermaid-svg-ZsQdfYWaMzfKvNeA .label text,#mermaid-svg-ZsQdfYWaMzfKvNeA span{fill:#333;color:#333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .node rect,#mermaid-svg-ZsQdfYWaMzfKvNeA .node circle,#mermaid-svg-ZsQdfYWaMzfKvNeA .node ellipse,#mermaid-svg-ZsQdfYWaMzfKvNeA .node polygon,#mermaid-svg-ZsQdfYWaMzfKvNeA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZsQdfYWaMzfKvNeA .rough-node .label text,#mermaid-svg-ZsQdfYWaMzfKvNeA .node .label text,#mermaid-svg-ZsQdfYWaMzfKvNeA .image-shape .label,#mermaid-svg-ZsQdfYWaMzfKvNeA .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZsQdfYWaMzfKvNeA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZsQdfYWaMzfKvNeA .rough-node .label,#mermaid-svg-ZsQdfYWaMzfKvNeA .node .label,#mermaid-svg-ZsQdfYWaMzfKvNeA .image-shape .label,#mermaid-svg-ZsQdfYWaMzfKvNeA .icon-shape .label{text-align:center;}#mermaid-svg-ZsQdfYWaMzfKvNeA .node.clickable{cursor:pointer;}#mermaid-svg-ZsQdfYWaMzfKvNeA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .arrowheadPath{fill:#333333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZsQdfYWaMzfKvNeA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZsQdfYWaMzfKvNeA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZsQdfYWaMzfKvNeA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZsQdfYWaMzfKvNeA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZsQdfYWaMzfKvNeA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZsQdfYWaMzfKvNeA .cluster text{fill:#333;}#mermaid-svg-ZsQdfYWaMzfKvNeA .cluster span{color:#333;}#mermaid-svg-ZsQdfYWaMzfKvNeA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZsQdfYWaMzfKvNeA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZsQdfYWaMzfKvNeA rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZsQdfYWaMzfKvNeA .icon-shape,#mermaid-svg-ZsQdfYWaMzfKvNeA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZsQdfYWaMzfKvNeA .icon-shape p,#mermaid-svg-ZsQdfYWaMzfKvNeA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZsQdfYWaMzfKvNeA .icon-shape .label rect,#mermaid-svg-ZsQdfYWaMzfKvNeA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZsQdfYWaMzfKvNeA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZsQdfYWaMzfKvNeA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZsQdfYWaMzfKvNeA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

acquire()
计数 > 0 ?
计数减 1

继续执行
阻塞等待
有 release() 调用
release()
计数加 1

唤醒等待者

关键 API:

  • acquire() ------ P 操作,计数减 1,不足时阻塞
  • release(n = 1) ------ V 操作,计数加 n,唤醒等待者
  • try_acquire() ------ 非阻塞版 acquire,失败返回 false
  • try_acquire_for(dur) ------ 带超时的 acquire

典型场景:线程池任务队列限流、连接池资源分配、生产者-消费者协调。

五、与 C++11 条件变量方案对比

5.1 latch vs. condition_variable

用条件变量实现"等待 N 个线程完成":

cpp 复制代码
// C++11 方式
int count = 3;
std::mutex mtx;
std::condition_variable cv;
{
    std::unique_lock lk{mtx};
    if (--count == 0)
        cv.notify_all();
    else
        cv.wait(lk, [&]{ return count == 0; });
}

问题:

  • 需要额外 mutexint 计数器
  • 虚假唤醒(spurious wakeup)必须用谓词循环检查
  • notify_all() 唤醒所有等待者,即使计数没到零

std::latch

cpp 复制代码
std::latch done{3};
done.count_down();     // 任何线程
done.wait();           // 等待线程
  • 零额外变量,零虚假唤醒
  • 底层通常基于 std::atomic<int> + futex,无锁路径

5.2 barrier vs. 手写循环

手写可复用栅栏需要条件变量 + 计数器 + 轮次标志,逻辑极易出错。std::barrier 封装了全部状态机,且完成回调的执行保证:恰好有一个线程执行回调,其他线程在回调完成后才被放行。

5.3 semaphore vs. condition_variable

维度 condition_variable counting_semaphore
状态 无状态,需外部变量 自带计数状态
虚假唤醒 可能发生,需谓词检查 不产生
多资源管理 需自行计算 acquire/release 原生支持
超时 支持 wait_for/until 同上

六、面试常考题

Q1:latch 和 barrier 的根本区别是什么?

latch 一次性使用,计数归零后永久放行;barrier 每轮结束后自动重置,支持多轮同步且可设完成回调。

Q2:barrier 的 completion function 由哪个线程执行?

由每轮最后一个调用 arrive_and_wait() 的线程执行,且执行期间其他线程仍处于阻塞状态。

Q3:binary_semaphore 可以替代 mutex 吗?

不能直接替代。binary_semaphore 的 release() 可以在任意线程调用,而 mutex 要求同一线程 unlock。但正因如此,binary_semaphore 适合实现信号量传递(如生产者通知消费者)。

Q4:counting_semaphore<5> 和 counting_semaphore<10> 在运行时有区别吗?

没有。模板参数只是最大计数的下限声明,运行时行为取决于初始值和 release 调用。不同特化类型的对象也不能相互赋值。

Q5:C++20 的并发原语能替代 std::future 吗?

不能完全替代。latch/barrier 关注线程同步点,future 关注异步结果传递。它们解决的问题正交,常配合使用。

Q6:什么场景下 barrier 比 latch 更适合?

任何需要多轮同步的场景:比如迭代求解(每轮结束后广播中间结果)、帧动画渲染(每帧多线程分工后合并、下一帧继续)。latch 只适合一次性的"等待初始化完成"或"等待所有任务提交"。

七、总结

latch/barrier/semaphore 填补了 C++11 标准库在同步原语上的空白。它们比手写条件变量方案更简洁、性能更好(无虚假唤醒、原子操作更快),并且语义明确、不易出错。在新项目中,凡是遇到"等待 N 个线程"或"限制并发数量"的需求,优先考虑这三个标准原语。