现代 C++ 实战(13):C++20 同步原语
第 12 篇用 thread、mutex、condition_variable 搭好了并发地基。C++20 补上最后几块拼图:jthread 自动 join 与协作式取消、semaphore 限流、latch / barrier 多线程阶段同步——写法更短、语义更清晰。
这一篇对应 demo:ref/cpp_demo/concurrency/sync_primitives/。
这是「现代 C++ 实战」系列的第 13 篇。建议先读 第 12 篇:多线程基础。
一、C++20 补上了什么?
| 原语 | 解决的问题 |
|---|---|
std::jthread |
忘记 join() 导致 terminate;需要优雅停止后台线程 |
std::counting_semaphore |
限制同时访问资源的线程数(连接池、限流) |
std::latch |
一次性「等所有人到齐」 |
std::barrier |
可重复的多阶段同步点 |
std::stop_token |
协作式取消(配合 jthread) |
编译要求:-std=c++20(demo 的 CMake 里 CMAKE_CXX_STANDARD 20)。demo README 写的是 GCC 10+、Clang 10+、MSVC 2019+,但标准库落地时间并不一致——libstdc++ 要到 GCC 11 才有 <semaphore> / <latch> / <barrier>,libc++ 的 jthread 更晚。所以 demo 用 __has_include(<stop_token>) 等做条件编译,缺哪个头文件就把对应演示换成一句警告。Docker 镜像里的 GCC 15 / Clang 22 + libc++ 四项全部支持。
二、std::jthread:自动 join + stop_token
std::thread 析构时若未 join/detach 会 std::terminate。jthread 析构时自动 join。
1 |
|
std::thread |
std::jthread |
|
|---|---|---|
| 析构 | 未 join → terminate | 自动 join |
| 取消 | 无标准机制 | request_stop() + stop_requested() |
| 适用 | 简单 fire-and-forget(需手动 join) | 后台服务、线程池 worker |
协作式取消:线程必须主动检查 stop_requested(),不能强制杀线程——与 第 12 篇 的 mutex 保护一样,是「约定式」安全退出。
三、std::stop_token 与 std::stop_source
1 | std::stop_source src; |
stop_source:发起取消的一方(通常是主线程或管理器)stop_token:只读视图,传给工作线程- 多个线程可共享同一个
stop_source派生的 token(显式传进去),一次request_stop()全部通知
坑:如果 lambda 的第一个参数是 std::stop_token st,jthread 传进来的是它自己内部 stop_source 的 token,和外部的 src 毫无关系——外部 src.request_stop() 不会让 st.stop_requested() 变真。要用外部 source,就只检查捕获的 token;要用 jthread 自带的,就调用 t.request_stop()(或 t.get_stop_source())。
demo 的 demo_jthread() 把这两种用法分开演示:
1 | // 方式 1:jthread 自带的 stop_token |
运行输出(时间戳略):
1 | [方式1] 主线程调用 worker.request_stop() |
stop_callback 在 request_stop() 的调用线程里同步执行,所以它总是先于工作线程的退出日志出现。
四、std::counting_semaphore:资源计数与限流
信号量维护一个非负计数:
acquire():计数减 1;若为 0 则阻塞release():计数加 1,唤醒等待者
1 |
|
| 类型 | 等价 | 用途 |
|---|---|---|
counting_semaphore<N> |
计数上限 N | 连接池、限流、多槽位资源 |
binary_semaphore |
counting_semaphore<1> |
类似 mutex,但可 release 由不同线程 |
与 mutex 对比:mutex 同一时刻只允许 1 个线程;semaphore 允许 k 个——典型场景是「数据库连接池最多 10 条连接」。
五、std::latch:一次性倒计时
1 |
|
- 构造时设初始计数(如 worker 数量)
- 每个线程
count_down()一次 - 所有线程
wait()直到计数为 0 - 只能用一次——适合「全员初始化完毕再开工」
demo 的 demo_latch() 是 5 个 worker + std::latch ready_latch(num_workers),主线程也调用 ready_latch.wait() 等全员就绪;demo_semaphore() 则是 6 个线程抢 std::counting_semaphore<3> 的 3 个槽位。
用 C++11 的 mutex + condition_variable 也能写同样逻辑,但 latch 语义专一、代码更短(demo 中有对比)。
六、std::barrier:可重复的多阶段同步
1 |
|
latch |
barrier |
|
|---|---|---|
| 重用 | 一次性 | 每轮 arrive_and_wait 后重置 |
| 典型场景 | 启动屏障 | 并行算法的多轮迭代 |
| 回调 | 无 | 可有 completion_function |
demo 的 demo_barrier():4 个线程 × 3 个阶段,std::barrier phase_barrier(num_threads, []() { ... }) 的回调在每阶段全员到齐后打印一行提示。
适合:并行排序的分段归并、迭代仿真中「每步全员同步再继续」。
七、与 C++11 原语:什么时候升级?
| 需求 | C++11 | C++20 推荐 |
|---|---|---|
| 互斥保护共享数据 | mutex |
仍用 mutex |
| 等待条件 / 队列 | condition_variable |
仍适用;复杂队列可保留 |
| 后台线程 + 优雅退出 | thread + 原子标志 |
jthread + stop_token |
| 限制并发数 | 手写计数 + cv | counting_semaphore |
| 全员到齐(一次) | mutex + cv + 计数 | latch |
| 多阶段同步 | 同上,易错 | barrier |
原则:C++11 原语并未过时;C++20 是在常见模式上提供专用类型,减少样板代码和 bug。
八、demo 运行
1 | cd ref/cpp_demo/concurrency/sync_primitives |
程序(单一 target sync_primitives,源码只有 sync_primitives.cpp)会依次调用 demo_jthread()、demo_semaphore()、demo_latch()、demo_barrier(),最后用 demo_traditional_sync() 演示 C++11 mutex + condition_variable 手写同步点作对比。启动时会打印编译器对各特性的支持情况(✓/✗)。
九、小结
| 组件 | 一句话 |
|---|---|
jthread |
自动 join,配合 stop_token 协作取消 |
semaphore |
计数限流,控制并发访问数 |
latch |
一次性「等 N 个线程就绪」 |
barrier |
可重复的多阶段同步点 |
现代 C++ 实战系列第 13 篇完。下一篇 线程池与背压控制——有界队列才是生产级关键。
系列导航
| 篇号 | 标题 | 状态 |
|---|---|---|
| 12 | 多线程基础 | ✅ |
| 13 | C++20 同步原语(本篇) | ✅ |
| 14 | 线程池与背压控制 | ✅ |











