第 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <chrono>
#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_token 与 std::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] { // 检查的是外部 source 派生的 tok
while (!tok.stop_requested()) { /* … */ }
});

src.request_stop(); // 所有从该 source 派生的 token 都会收到停止请求
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方式 1:jthread 自带的 stop_token
std::jthread worker([](std::stop_token st) {
while (!st.stop_requested()) { /* 干活 */ }
});
worker.request_stop(); // 通知的是 worker 内部的 stop_source

// 方式 2:外部 stop_source 统一取消多个线程
std::stop_source stop_src;
std::vector<std::jthread> workers;
for (int id = 1; id <= 2; ++id) {
workers.emplace_back([id, tok = stop_src.get_token()] {
while (!tok.stop_requested()) { /* 干活 */ } // 检查的是捕获的外部 token
});
}
std::stop_callback on_stop(stop_src.get_token(), [] {
/* 取消时立即回调,适合唤醒阻塞等待、释放资源 */
});
stop_src.request_stop(); // 一次通知所有线程
workers.clear(); // jthread 析构自动 join

运行输出(时间戳略):

1
2
3
4
5
6
7
[方式1] 主线程调用 worker.request_stop()
[方式1] 工作线程收到停止信号,优雅退出
[方式2] 主线程调用 stop_src.request_stop()
[方式2] stop_callback 被触发
[方式2] 线程 1 收到外部停止信号,共循环 6 次
[方式2] 线程 2 收到外部停止信号,共循环 6 次
[方式2] 所有工作线程已结束

stop_callback 在 request_stop() 的调用线程里同步执行,所以它总是先于工作线程的退出日志出现。

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

信号量维护一个非负计数:

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

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

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
13
14
#include <latch>

const int N = 5;
std::latch ready(N);
void init(int i);
void run_task(int i);

// 每个 worker:
void worker(int i) {
init(i);
ready.count_down(); // 计数 -1
ready.wait(); // 等计数变 0
run_task(i);
}
  • 构造时设初始计数(如 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <barrier>

void on_phase_complete() noexcept;
void do_phase_work(int id, int phase);

std::barrier sync(4, []() noexcept { // 回调要求 noexcept 可调用
// 可选:每阶段所有线程到齐后执行的回调
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

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

程序(单一 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 线程池与背压控制 ✅