C++ STL 之 latch / barrier / semaphore 详解(C++20 并发新原语)
C++ STL 之 latch / barrier / semaphore 详解(C++20 并发新原语)
一、为什么需要新的并发原语
C++11 提供了 std::thread、std::mutex、std::condition_variable 和 std::future,但这些工具在面对"等待一组任务完成"、"限制并发数量"等常见场景时,要么需要手写复杂的同步逻辑,要么性能不够理想。C++20 从 POSIX 和 Java 等生态引入了三个标准库同步原语:
| 原语 | 作用 | 是否可复用 |
|---|---|---|
std::latch |
一次性同步点,N 个线程到达后放行 | 否 |
std::barrier |
可复用同步点,每轮到达触发回调 | 是 |
std::counting_semaphore |
计数信号量,控制资源访问数 | 是 |
它们都定义在 <latch>、<barrier>、<semaphore> 头文件中,底层通常基于原子操作或 futex 实现,比条件变量方案更轻量。
二、latch——一次性栅栏
std::latch 是一个向下计数的一次性屏障。构造时指定初始计数,线程调用 count_down() 减少计数,调用 wait() 阻塞直到计数归零。计数到达零后永久保持,不可重置。
#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()—— 阻塞直到计数为 0arrive_and_wait(n = 1)—— 等价于count_down(n)+wait()try_wait()—— 非阻塞检查计数是否为 0
典型场景:初始化加载、多路并行计算后合并结果、所有线程就绪后同时开始。
三、barrier——可复用栅栏
std::barrier 是 latch 的升级版。它像一个"回合制"同步点:每轮 N 个线程全部到达后,一起放行,然后自动进入下一轮。每轮结束时还会执行一个完成回调(completion function)。
#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 的核心保证。
关键 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>。
#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";
}
}
关键 API:
acquire()—— P 操作,计数减 1,不足时阻塞release(n = 1)—— V 操作,计数加 n,唤醒等待者try_acquire()—— 非阻塞版 acquire,失败返回 falsetry_acquire_for(dur)—— 带超时的 acquire
典型场景:线程池任务队列限流、连接池资源分配、生产者-消费者协调。
五、与 C++11 条件变量方案对比
5.1 latch vs. condition_variable
用条件变量实现"等待 N 个线程完成":
// 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; });
}
问题:
- 需要额外
mutex和int计数器 - 虚假唤醒(spurious wakeup)必须用谓词循环检查
notify_all()唤醒所有等待者,即使计数没到零
用 std::latch:
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 个线程"或"限制并发数量"的需求,优先考虑这三个标准原语。
更多推荐



所有评论(0)