第 12 篇threadmutexcondition_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

编译要求:GCC 10+、Clang 10+、MSVC 2019+,-std=c++20

二、std::jthread:自动 join + stop_token

std::thread 析构时若未 join/detach 会 std::terminatejthread 析构时自动 join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <thread>
#include <stop_token>

void worker(std::stop_token st) {
while (!st.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 做周期性工作…
}
// 清理资源后退出
}

int main() {
std::jthread t(worker); // 自动把 stop_token 传给 worker

std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop(); // 请求协作式停止
// t 析构时自动 join
}
std::thread std::jthread
析构 未 join → terminate 自动 join
取消 无标准机制 request_stop() + stop_requested()
适用 简单 fire-and-forget(需手动 join) 后台服务、线程池 worker

协作式取消:线程必须主动检查 stop_requested(),不能强制杀线程——与 第 12 篇 的 mutex 保护一样,是「约定式」安全退出。

三、std::stop_tokenstd::stop_source

1
2
3
4
5
6
7
8
std::stop_source src;
std::stop_token tok = src.get_token();

std::jthread t([tok](std::stop_token st) {
while (!st.stop_requested()) { /* … */ }
});

src.request_stop(); // 所有从该 source 派生的 token 都会收到停止请求
  • stop_source:发起取消的一方(通常是主线程或管理器)
  • stop_token:只读视图,传给工作线程
  • 多个 jthread 可共享同一个 stop_source,一次 request_stop() 全部通知

四、std::counting_semaphore:资源计数与限流

信号量维护一个非负计数

  • acquire():计数减 1;若为 0 则阻塞
  • release():计数加 1,唤醒等待者
1
2
3
4
5
6
7
8
9
10
#include <semaphore>

std::counting_semaphore<3> sem(3); // 最多 3 个并发

void use_resource(int id) {
sem.acquire();
// 临界区:最多 3 个线程同时在此
do_work();
sem.release();
}
类型 等价 用途
counting_semaphore<N> 计数上限 N 连接池、限流、多槽位资源
binary_semaphore counting_semaphore<1> 类似 mutex,但可 release 由不同线程

mutex 对比:mutex 同一时刻只允许 1 个线程;semaphore 允许 k 个——典型场景是「数据库连接池最多 10 条连接」。

五、std::latch:一次性倒计时

1
2
3
4
5
6
7
8
9
10
11
12
#include <latch>

const int N = 5;
std::latch ready(N);

// 每个 worker:
void worker(int i) {
init(i);
ready.count_down(); // 计数 -1
ready.wait(); // 等计数变 0
run_task(i);
}
  • 构造时设初始计数(如 worker 数量)
  • 每个线程 count_down() 一次
  • 所有线程 wait() 直到计数为 0
  • 只能用一次——适合「全员初始化完毕再开工」

用 C++11 的 mutex + condition_variable 也能写同样逻辑,但 latch 语义专一、代码更短(demo 中有对比)。

六、std::barrier:可重复的多阶段同步

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <barrier>

std::barrier sync(4, [] {
// 可选:每阶段所有线程到齐后执行的回调
on_phase_complete();
});

void worker(int id) {
for (int phase = 0; phase < 3; ++phase) {
do_phase_work(id, phase);
sync.arrive_and_wait(); // 到齐后进入下一阶段
}
}
latch barrier
重用 一次性 每轮 arrive_and_wait 后重置
典型场景 启动屏障 并行算法的多轮迭代
回调 可有 completion_function

适合:并行排序的分段归并、迭代仿真中「每步全员同步再继续」。

七、与 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
2
cd ref/cpp_demo/concurrency/sync_primitives
./build.sh --run

程序会依次演示 jthread、semaphore、latch、barrier,以及与 C++11 mutex/cv 的对比。启动时会打印编译器对各特性的支持情况(✓/✗)。

九、小结

组件 一句话
jthread 自动 join,配合 stop_token 协作取消
semaphore 计数限流,控制并发访问数
latch 一次性「等 N 个线程就绪」
barrier 可重复的多阶段同步点

现代 C++ 实战系列第 13 篇完。下一篇 线程池与背压控制——有界队列才是生产级关键。

系列导航

篇号 标题 状态
12 多线程基础
13 C++20 同步原语(本篇)
14 线程池与背压控制 下一篇

完整大纲见工作区 docs/CPP_SERIES_OUTLINE.md