thread与jthread机制
# 44.thread与jthread机制
# 目录介绍
- 1. 案例引入
- 2. 架构概览
- 3. std::thread 的核心语义
- 4. thread 的 pthread 底层映射
- 5. thread_local 在子线程中的初始化时机
- 6. jthread 的 join 自动保证
- 7. stop_token 协作取消机制——深层原理
- 8. jthread 与 thread 的完整对决
- 9. 常见陷阱与反模式
- 10. 综合案例串讲
# 1. 案例引入
# 1.1 ~thread 崩溃——join 或 detach 必须选一个
某量化引擎的交易网关模块,用 std::thread 管理行情接收线程。在一次网络超时后的重启逻辑里,代码出现了偶然崩溃:
// ====== 事故代码 V1:joinable 的 thread 析构 → std::terminate ======
class MarketDataGateway {
std::thread receiver_thread_;
void start() {
receiver_thread_ = std::thread([this] { receive_loop(); });
}
void stop() {
if (connected_) {
disconnect();
// ❌ 忘记 join 或 detach——直接退出 stop()
}
} // ~MarketDataGateway → ~thread → receiver_thread_.joinable() == true
// → std::terminate() 被调用 → 整个进程 SIGABRT
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
崩溃轨迹:
时刻 T0:stop() 被调用 → disconnect() 关闭 socket
→ receiver_thread_ 仍在 receive_loop 中 read socket → 返回 -1
时刻 T1:stop() 返回 → ~MarketDataGateway 开始
→ ~thread() 被调用
→ 检查 joinable() → true(线程仍在运行)
→ std::terminate() → SIGABRT → 整个进程炸掉
→ 不仅是这个模块——整个交易引擎崩溃!
2
3
4
5
6
7
8
关键数据:这个 bug 在生产环境潜伏了 3 个月。因为正常情况下 receive_loop 会在 disconnect() 后很快从 read() 返回——大部分时间 stop() 之后几微秒线程就结束了。只有在网络抖动严重时(read() 陷入长时间 blocking),线程在 ~thread() 时还没退出——此时触发崩溃。偶然性 × 业务致命性 = 最难修的一类 bug。
# 1.2 jthread协作取消丢失
同一个团队后来升级到 C++20,把 thread 换成 jthread——本以为自动 join 就能高枕无忧。但在实际运行中遇到了新问题:
// ====== 事故代码 V2:jthread 的 stop 不生效 ======
void data_pipeline() {
std::jthread worker([](std::stop_token st) {
while (!st.stop_requested()) { // ① 检查停止请求
auto batch = fetch_next_batch();
if (batch.empty()) continue;
process_batch(batch); // ② 耗时操作——可能持续 10 秒
}
});
// ... 运行一段时间后
// ③ worker 的析构自动调 request_stop() + join()
// 但 join 在 process_batch 返回之前一直阻塞——最长 10 秒!
}
2
3
4
5
6
7
8
9
10
11
12
13
14
根因:stop_token 只在协作点被检查——如果线程正在执行一个不可分割的耗时操作(如 process_batch),停止请求会排在下一次 stop_requested() 检查之后。最坏情况下延迟 = 单次 process_batch 的最大耗时。
深层表达:jthread 的协约是「我请求你停止,你尽快停下来」——不是「我请求你停止,你立刻停下来」。协作取消不是抢占取消——这是设计选择,不是缺陷。 前者保持了一致性(不破坏正在处理的数据),后者破坏了完整性但保证了响应性。C++ 选择了前者。
# 1.3 八个待解疑问
① thread 构造后线程什么时候开始执行?是立即吗? → 第 3 章
② 为什么 thread 析构时如果 joinable 就直接 terminate?不能替我们 join? → 第 3 章
③ thread 为什么不能拷贝?所有权语义和 pthread_t 有什么关系? → 第 3 章
④ thread 在 Linux 上怎么映射到 pthread?pthread_create 和 join 的内核真相?→ 第 4 章
⑤ thread_local 变量在子线程里什么时候初始化?和主线程有什么区别? → 第 5 章
⑥ jthread 的自动 join 是怎么做到的?会阻塞多久? → 第 6 章
⑦ stop_token 是怎么在多个线程间安全传递停止信号的?原子操作藏在哪? → 第 7 章
⑧ jthread 出来之后 thread 还有什么存在意义?什么场景还该用 thread? → 第 8 章
2
3
4
5
6
7
8
# 2. 架构概览
# 2.1 thread 和 jthread 的三层模型
┌──────────────────────────────────────────────────────────┐
│ 用户代码层 │
│ std::thread t(f, args...) std::jthread jt(f, args...)│
│ - join / detach / joinable - 自动 join │
│ - get_stop_source/token │
└──────────────┬───────────────────┬───────────────────────┘
│ │
┌──────────────▼───────────────────▼───────────────────────┐
│ C++ 标准库层 │
│ __thread_data (内部) __jthread_data (内部) │
│ - 管理 native_handle - 管理 stop_source │
│ - joinable 状态 - 在析构时自动 request │
│ - 调用父类 join │
└──────────────┬───────────────────┬───────────────────────┘
│ │
┌──────────────▼───────────────────▼───────────────────────┐
│ OS 内核层 │
│ Linux: pthread_create / pthread_join / pthread_detach │
│ Windows: CreateThread / WaitForSingleObject │
│ macOS: 同样用 pthread(POSIX 兼容) │
│ │
│ 内核对象:task_struct (Linux) / ETHREAD (Windows) │
│ - 线程 ID (tid) / 调度状态 / 内核栈 │
│ - futex 等待队列(用于 join 时的等待语义) │
└──────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
三层之间的关键关系:
- 用户层的
std::thread对象不是内核线程——它是内核线程的 RAII handle。 sizeof(std::thread)在 Linux x86-64 上通常只有 8 字节(存储pthread_t即unsigned long)。- 移动语义:
std::thread是唯一所有权转移——移动后源对象变成无效线程(joinable() == false)。
# 2.2 为何这么切
疑惑:为什么 C++11 不给 thread 自动 join?到了 C++20 才推出 jthread?
论证——三个维度的渐进式认识:
C++11 时代的设计考量:
① 自动 join 意味着析构函数可能长时间阻塞——违背了 RAII 的 "析构应该快"
② 自动 join 掩盖了逻辑错误——线程仍在执行但已经被认为"结束"
③ 自动 detach 意味着线程脱离控制——可能访问已析构的栈对象
选择:让程序员显式选择——join 或 detach。不选 = std::terminate。硬错误优于暗中错误。
C++20 时代的反思:
① 社区 10 年实践表明——非 joinable 析构崩溃是最常见的并发 bug 之一
② stop_token 机制成熟了——可以优雅停止线程,join 不再意味着无限等待
③ C++20 有 stop_callback 集成 condition_variable——可中断等待成为可能
选择:推出 jthread——自动 request_stop + join。把 10 年的经验教训编码进类型系统。
2
3
4
5
6
7
8
9
10
11
12
13
结论:thread 是「自由的枪」——给你一切权力但一切责任也是你的。jthread 是「安全的约束」——帮你打下安全网,同时通过 stop_token 给你优雅退出的机制。C++ 的学习轨迹就是从学习「枪怎么用」到理解「网为什么存在」。
# 3. std::thread 的核心语义
# 3.1 构造:移动不可拷贝与线程立即启动
std::thread t1(func, arg1, arg2); // ① 构造 → 线程立即启动
// 拷贝构造 = delete
// std::thread t2 = t1; // ❌ 编译错误
// 移动构造——所有权转移
std::thread t3 = std::move(t1); // ✅ t1.joinable() == false,t3 拥有线程
2
3
4
5
6
7
为什么线程构造 = 立即启动? 不采用「两阶段构造」(先构造对象、再 start()):
C++ 的设计选择——RAII 的铁律:
构造 = 获取资源。线程资源在构造时获取——线程立即运行。
如果允许延迟启动:
auto t = std::thread(func); // 构造——但线程还没跑?
// ... 中间可能抛异常
t.start(); // 启动了但可能永远不会调用?
→ 如果中间抛异常——t 析构时的状态无法确定(跑了还是没跑?)
2
3
4
5
6
7
8
构造时的参数转发——内部机制:
template <typename Callable, typename... Args>
thread::thread(Callable&& f, Args&&... args) {
// ① decay_copy 参数——防止引用悬垂
auto decayed = std::make_tuple(std::decay_t<Args>(std::forward<Args>(args))...);
// ② 通过「调用包装器」存放——保证参数的生命周期到线程入口
// ③ pthread_create 或 CreateThread——操作系统调用
// ④ 线程入口解锁参数、调用 f(args...)
}
2
3
4
5
6
7
8
9
为什么参数要 decay_copy 而不是引用?
void bad(int& x) {
std::thread t([&] { ++x; }); // ❌ lambda 用引用——x 可能在线程运行时已析构
}
void good(int& x) {
std::thread t([](int& x) { ++x; }, std::ref(x));
// std::ref 告诉 thread:「这个参数我用引用传递——我知道自己在干什么」
}
2
3
4
5
6
7
8
# 3.2 析构:joinable → std::terminate 的硬规则
~thread() {
if (joinable()) {
std::terminate(); // ⚠️ 不是「忘了 join」——是「严重逻辑错误」
}
}
2
3
4
5
疑惑:为什么标准选择 std::terminate 而不是自动 join?自动 join 不是更安全?
论证——三个层面:
层面 1:自说明性——显式 join 表明「我在这里等线程结束」
自动 join 意味着析构函数可能阻塞任意长时间。
一个看起来没有副作用的 }(析构结束)可能让程序卡住——违反直觉。
层面 2:错误检测——terminate 告诉你「这里有 bug」
如果你忘了 join——terminate 崩溃。崩溃是 bug 被发现——比静默阻塞好。
阻塞意味着程序「看起来活着但实际卡住了」——更难排查。
层面 3:异常安全——join 可以在正确的位置处理异常
try {
t.join(); // 我可以在这里处理线程的异常
} catch (...) { /* 某个合理的异常处理点 */ }
// 如果在析构里自动 join——异常从析构函数抛出 → std::terminate
2
3
4
5
6
7
8
9
10
11
12
13
核心结论:std::terminate 不是惩罚——是最响亮的告警。它告诉你「你的线程生命周期管理有漏洞——修好它」。
# 3.3 join vs detach 的生死边界
join = 「我等你结束」→ 阻塞直到线程函数返回 → 回收线程资源
join 之后 → thread 对象不再关联任何线程 → joinable() = false
detach = 「你不用回来了」→ 线程与 thread 对象分离 → 线程独立运行
detach 之后 → thread 对象不再关联任何线程 → joinable() = false
线程成为「守护线程」——在进程结束时被操作系统回收
2
3
4
5
6
join 的汇编本质:
std::thread t(worker);
t.join();
2
; t.join() → glibc 内部:
mov rdi, [rsp+8] ; rdi = thread 对象中的 native_handle (pthread_t)
call pthread_join ; → futex(FUTEX_WAIT, ...) 等待 tid 对应的 futex
; → 线程结束时内核唤醒等待者
test eax, eax
jne throw_system_error
2
3
4
5
6
join 的阻塞模型:调用线程进入 TASK_UNINTERRUPTIBLE(Linux)——不响应信号、不可被中断、直到被等待的线程结束。这是内核级的保证——与用户态的自旋完全不同。
# 3.4 为什么 thread 不能拷贝——所有权与物理实体相等
疑惑:std::string 可以拷贝——为什么 std::thread 不行?不都只是资源管理吗?
论证——三类资源的拷贝性对比:
类型 1:值资源——拷贝 = 复制值
std::string: 拷贝 → 两个独立的 char[],内容相同。正确 + 安全。
类型 2:共享资源——拷贝 = 共享所有权
std::shared_ptr: 拷贝 → 两个指针共享一个引用计数。正确 + 安全。
类型 3:独占物理资源——拷贝 = 逻辑不可能
std::thread: 拷贝 → 同一个内核线程被两个对象管理。
后果:第一个析构时 join 或 detach——第二个析构时怎么处理?
已经结束了?已经被 detach 了?无法定义一致语义。
2
3
4
5
6
7
8
9
10
pthread_t 是不能拷贝的物理实体——它是一个内核对象的 ID。两个 std::thread 对象共享同一个 pthread_t 意味着它们对同一个内核线程拥有冲突的控制权(一个 join 了另一个就不能 join)。
C++ 的选择:移动语义——唯一的正确方式。移动把所有权从一个对象转移到另一个——源对象变成「空」状态。这和物理世界中「转让一个线程」的语义完全一致。
# 4. thread 的 pthread 底层映射
# 4.1 从 thread 构造到 pthread_create 的全链路
std::thread t([] { /* worker */ });
这条语句在 Linux (glibc) 上的完整调用链:
用户层:
std::thread::thread(Callable&& f)
→ 创建 __thread_data 对象
→ decay_copy 参数打包进 tuple
→ 包装成调用适配器
标准库层(libstdc++):
__gthread_create(native_handle, thread_proxy, data_ptr)
→ 创建线程属性:PTHREAD_CREATE_DETACHED = 0 (joinable)
→ pthread_create(&tid, &attr, &thread_proxy, data_ptr)
系统调用层:
① clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
CLONE_THREAD | CLONE_SYSVSEM, ...)
→ 创建新的 task_struct(共享同一地址空间 + 文件表)
② 内核为新线程分配独立的:
- 内核栈(通常 16KB)
- 线程 ID (tid = pid 新语义——Linux 2.6+ 中线程和进程统一为 task)
- 调度实体(sched_entity)
③ 新线程被加入调度器的 runqueue → 等待 CPU 调度
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
关键细节——新线程什么时候真正执行?
clone() 返回后——新线程已存在于内核的 runqueue 中。
但 CPU 什么时候调度它——完全取决于调度器的负载均衡和优先级。
从 clone() 返回到新线程第一条指令执行——通常在几微秒到几十微秒之间。
但线程创建的开销(clone + task_struct 初始化)本身约 10-15 微秒——远大于锁的开销。
2
3
4
5
# 4.2 join 如何等待——pthread_join 的内核机制
t.join();
// → pthread_join(t.native_handle(), nullptr)
2
内核层的完整等待流程:
① pthread_join(tid, NULL) → 检查 tid 是否存在且 joinable
如果 tid 已经被 join 过 → return EINVAL
如果 tid 已经被 detach → return EINVAL
② 检查目标线程是否已经结束(task_struct 的 exit_state)
如果已结束 → 立即返回(无需睡眠)
如果未结束 → 进入睡眠等待
③ 睡眠机制——Linux 的 do_wait_task():
调用者:set_current_state(TASK_UNINTERRUPTIBLE)
注册为 tid 对应 task_struct 的「等待者」
调用 schedule() → 让出 CPU
④ 目标线程结束时:
do_exit() → 检查是否有等待者在等待我
→ 唤醒等待者 → wake_up_process(waiter)
→ 调用者从 schedule() 返回 → 重新调度
⑤ pthread_join 返回:
目标线程的资源已被回收(内核栈、task_struct 放回 slab)
joinable 状态清除
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
汇编证据——join 的完整汇编:
; t.join() 在 glibc 中的简化汇编
mov rdi, [rbx] ; rdi = pthread_t = native_handle
xor esi, esi ; esi = NULL(不需要返回值指针)
call pthread_join
; pthread_join 内部:
; mov eax, [rdi] ; 读 tid
; test eax, eax ; 检查是否为 0(空线程)
; jz error
; call __pthread_join_internal
; 进入内核:futex(FUTEX_WAIT, tid_futex_addr, ...)
; 等待 tid 线程的 futex 被唤醒
2
3
4
5
6
7
8
9
10
11
# 4.3 native_handle 的用途与安全边界
std::thread t(worker);
pthread_t handle = t.native_handle(); // 拿到底层 pthread_t 的纯值
// 合法用法:
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(3, &cpuset);
pthread_setaffinity_np(handle, sizeof(cpuset), &cpuset); // 绑核
// 危险用法:
pthread_cancel(handle); // ⚠️ C++ 的异常机制 ≠ pthread cancel
// 析构函数可能不会被调用!
2
3
4
5
6
7
8
9
10
11
12
安全边界:native_handle 暴露给你是为了平台特定的操作(CPU 绑核、优先级设置、调度策略)——不是为了绕开 C++ 线程生命周期管理。
# 4.4 线程创建失败的三种原因与检测
void safe_create_thread() {
try {
std::thread t(worker);
t.join();
} catch (const std::system_error& e) {
// e.code() == std::errc::resource_unavailable_try_again
// 三种可能:
// ① RLIMIT_NPROC 限制——用户进程数上限
// ② 内存不足——内核栈分配失败
// ③ PID 耗尽——/proc/sys/kernel/pid_max
std::cerr << "thread creation failed: " << e.what() << '\n';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
常见触发场景:高并发服务启动时一次性创建 1000+ 线程——超出 ulimit -u。解决方案:线程池(复用线程)而非每次创建新线程。
# 5. thread_local 在子线程中的初始化时机
# 5.1 TLS 段的 Lazy 初始化模型
第 31 篇讲到了 TLS 的 FS 寄存器和 .tdata/.tbss 段。这里补充「子线程的初始化时机」:
主线程中 TLS 的初始化:
① 动态链接器在 main() 之前根据 .tdata 模板初始化
② 模块的初始化函数按依赖序调用
③ main() 开始前——所有 TLS 变量已构造
子线程中 TLS 的初始化——两种模式:
模式 A:静态 TLS(链接期可知)
线程创建时(clone 调用中)→ 从 TLS 模板拷贝 .tdata 值
→ 线程的第一条指令执行前——静态 TLS 变量已可用
→ 开销:clone 时多一次 memcpy(微秒级)
模式 B:动态 TLS(dlopen 引入的 .so 的 TLS)
线程访问 TLS 变量时→ 首次访问触发 __tls_get_addr()
→ 如果是第一次→ 分配 TLS 块、调用构造
→ 后续访问→ 直接返回已缓存的地址
→ 开销:首次访问有分配+构造成本
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.2 什么时候可能踩到未初始化的 TLS
// ❌ 危险——子线程的 TLS 可能被主线程的初始化函数「看到」
thread_local std::string tls_buffer;
void thread_func() {
tls_buffer = "worker"; // ✅ 首次使用——触发动态 TLS 初始化(如有必要)
}
int main() {
// 主线程的 tls_buffer 在此时已构造(静态 TLS——拷贝自模板)
std::thread t(thread_func);
t.join();
// 子线程的 tls_buffer 在线程创建时初始化——没问题
}
2
3
4
5
6
7
8
9
10
11
12
13
真正的危险场景——fork()(回扣第 31 篇):
thread_local std::string buffer = "init";
// 主线程:buffer 已构造
if (fork() == 0) {
// 子进程:buffer 的内存还在(copy-on-write),
// 但 pthread 的 TLS 初始化钩子不会被重新调用
// → 如果 buffer 是动态 TLS——访问可能触发 __tls_get_addr()
// → 这个函数在 fork 后的子进程可能返回错误地址!
buffer.append("child"); // ⚠️ 危险——只在 fork 后 execve 是安全的
}
2
3
4
5
6
7
8
9
10
# 5.3 与全局静态对象的线程安全对比
全局 static 对象(单份):
初始化在多线程环境下被 C++11 编译器保护(双重检查锁+futex)
每个线程访问同一份对象——不需要 TLS
thread_local 对象(每线程一份):
初始化在线程创建时——同一时刻只有一个线程在跑(创建它的线程)
→ 不需要锁!
子线程访问自己的副本——也不需要锁!
→ TLS = 编译器的物理隔离 = 天然的线程安全
2
3
4
5
6
7
8
9
# 6. jthread 的 join 自动保证
# 6.1 析构函数自动 request_stop + join
class jthread {
thread impl_; // 内部用 thread 实现
stop_source stop_source_; // 关联的停止源
public:
~jthread() {
if (joinable()) {
stop_source_.request_stop(); // ① 先请求停止
impl_.join(); // ② 再等待结束——最长等线程函数返回
}
}
// 在析构之前 thread 已 join → 没有 thread 的 terminate 风险
};
2
3
4
5
6
7
8
9
10
11
12
13
14
关键时序:
jthread 析构的精确顺序:
① request_stop() → stop_source 的原子标志设为 true
② join() → 等待线程函数返回(pthread_join → futex wait)
├─ 如果线程函数已经在 exit → 立即返回
└─ 如果线程函数还在跑 → 阻塞等待
③ 线程函数返回后 → join 完成 → impl_ 不再 joinable
④ jthread 析构完成
2
3
4
5
6
7
# 6.2 为什么这是 C++20 最安全的线程类型
疑惑:jthread 只是「析构时自动 join」——这有什么了不起?
论证——三重安全性提升:
安全性 1:消灭了最常见的 thread bug
忘记 join 或 detach → terminate 崩溃。
这个 bug 占所有 thread 相关 bug 的 ~40%(根据静态分析工具统计)。
jthread 从根源消灭了这个 bug 类——析构时自动处理。
安全性 2:request_stop 给了退出信号
thread 的 join 如果线程不退出——join 永远阻塞。
jthread 的 request_stop 给了线程「请退出」的信号。
线程可以检查 stop_token 来快速退出——而不是等超时或等 IO 返回。
安全性 3:最坏情况的阻塞有明确原因
thread: 忘了 join → terminate → 进程死
jthread: 析构阻塞 → 等待线程停止 → 如果线程不检查 stop_token,join 阻塞
同样是阻塞——但 jthread 的阻塞是「我在等线程优雅退出」。
这是语义上正确的等待——和 thread 的崩溃完全不同。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.3 从 thread 到 jthread 的最小迁移路径
// 迁移前——thread
class Service {
std::thread worker_;
void run() {
worker_ = std::thread([this] {
while (running_) { // 需要手写标志
auto data = wait_for_data();
if (data) process(data);
}
});
}
~Service() {
running_ = false; // 手动设标志
if (worker_.joinable()) worker_.join();
}
std::atomic<bool> running_{true}; // 手动管理停止状态
};
// 迁移后——jthread
class Service {
std::jthread worker_;
void run() {
worker_ = std::jthread([this](std::stop_token st) {
while (!st.stop_requested()) { // 用标准 stop_token
auto data = wait_for_data();
if (data) process(data);
// stop 被请求时下一轮循环退出
}
});
}
// ~Service(): ~jthread 自动 request_stop + join ✅
// 不需要 running_ 标志——stop_token 就是标准化的退出信号
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 7. stop_token 协作取消机制——深层原理
# 7.1 三组件:stop_source / stop_token / stop_callback
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────────┐
│ stop_source │ │ stop_token │ │ stop_callback │
│ (产生停止信号) │ │ (检测停止信号) │ │ (注册停止时回调) │
├─────────────────┤ ├─────────────────┤ ├──────────────────────┤
│ request_stop() │────► │ stop_requested() │ │ 回调函数 + 析构钩子 │
│ 发出停止请求 │ │ 返回 true/false │ │ stop 时在线程中被调用 │
│ │ │ │ │ │
│ 共享同一个 │◄─────│ 共享同一个 │◄─────│ 注册到同一个 │
│ stop_state │ │ stop_state │ │ stop_state │
└─────────────────┘ └─────────────────┘ └──────────────────────┘
2
3
4
5
6
7
8
9
10
内部实现——共享的 stop_state:
// libstdc++ 的 stop_state 简化版
struct stop_state {
atomic<uint32_t> state_; // 0 = 未停止, 1 = 已停止 + 有回调, 2 = 已停止+无回调
// 请求停止——原子操作
bool request_stop() {
uint32_t old = state_.exchange(1, memory_order_acq_rel);
if (old == 0) {
// 第一次请求——触发所有已注册的回调
invoke_callbacks();
state_.store(2, memory_order_release);
return true;
}
return false; // 已经停止——幂等返回
}
// 检查是否已停止
bool stop_requested() const {
return state_.load(memory_order_acquire) != 0;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7.2 内部共享状态的原子语义
stop_state 是 stop_source 和 stop_token 之间的共享桥梁:
stop_source 持有:shared_ptr<stop_state> —— 拥有所有权
stop_token 持有:weak_ptr<stop_state> —— 不拥有(token 先于 source 析构时安全)
原子状态转换:
0 (未停止) ── request_stop() ──► 1 (正在停止——触发回调)
1 ── 回调全部执行完 ──► 2 (已停止)
每个状态转换都是原子的——多线程同时调 request_stop 只有一个成功。
为什么用 shared_ptr 管理 stop_state:
source 可以在 token 之前析构——但如果 token 还活着,stop_state 必须活着。
token 中的 weak_ptr::lock() 返回 nullptr → 表示 source 已死 → 隐含「没有停止请求」
(这和 weak_ptr 在第 29/30 篇中的语义完全一致)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 7.3 stop_callback 的注册与析构顺序
std::stop_source src;
std::stop_callback cb(src.get_token(), [] { std::cout << "stopped\n"; });
// 注册时:
// 如果 src 已经停止了 → 回调立即在当前线程被调用
// 如果 src 还没停止 → 回调被注册到 stop_state 的回调链表
// cb 析构时:
// 如果还没被调用 → 从链表中移除(高效——不需要遍历,用双向链表)
// 如果已被调用 → 什么都不做
src.request_stop();
// → stop_state 遍历回调链表 → 依次调用每个回调
// → 回调在 request_stop 的线程中被调用——不是在被停止的线程
2
3
4
5
6
7
8
9
10
11
12
13
关键设计点——回调在哪个线程执行? 在调用 request_stop() 的线程(通常是主线程或管理线程)。这意味着 回调不应该阻塞 ——它应该在微秒级完成。
# 7.4 与 condition_variable 的集成——可中断等待
上一篇文章讲了条件变量的 wait 机制。C++20 新增了 wait_for 等函数的 stop_token 重载:
void worker(std::stop_token st) {
std::mutex mtx;
std::condition_variable_any cv; // any 版本支持任意锁
std::unique_lock lock(mtx);
// 可中断等待——语义:
// 等待 cv 被 notify 或者 stop 被请求
// 醒来后:
// 如果 stop 被请求 → 退出循环
// 如果 predicate 不满足 → 继续等待
cv.wait(lock, st, [] { return data_ready; });
}
2
3
4
5
6
7
8
9
10
11
12
内部实现——两种唤醒信号被合并在一个 wait 中:
cv.wait(lock, st, pred) 的完整流程:
① mutex unlock → 进入等待
② 等待两个来源的唤醒:
a. notify_one/notify_all → futex_wake
b. stop_source.request_stop → 注册 stop_callback 回调 → 在回调中调 notify_all
③ 被唤醒后 → mutex lock
④ 检查:st.stop_requested() || pred() → 任一为真 → 返回
2
3
4
5
6
7
为什么需要 condition_variable_any 而非 condition_variable? condition_variable 只支持 unique_lock<mutex>——而 stop_callback 的注册可能需要不同的锁类型。_any 版本支持任何满足 BasicLockable 的锁——为 stop_token 集成提供了灵活性。
# 7.5 为什么 stop 是不可逆的——设计意图
疑惑:为什么 request_stop() 不能撤销?像 cancel() 和 resume() 配对不是更灵活?
论证——不可逆的三个原因:
原因 1:状态简化
只需要 2 个状态(未停止 / 已停止)→ 原子语义极简
如果需要撤销 → 3 个状态(未 / 正在 / 已)→ 增加竞态复杂度
原因 2:语义诚实
停止 = 线程该退出。如果线程已经因为停止信号开始收尾(析构资源、写日志)
然后停止被撤销——这些收尾操作无法撤销。语义不一致。
原因 3:已经注册的回调
被调用的回调已产生副作用(close fd、写日志)——撤销没有意义
stop_callback 在 request_stop 时被立即调用——无法回滚
2
3
4
5
6
7
8
9
10
11
# 8. jthread 与 thread 的完整对决
# 8.1 七维全量对比表
| 维度 | std::thread | std::jthread |
|---|---|---|
| C++ 版本 | C++11 | C++20 |
| 析构时 joinable | std::terminate | 自动 request_stop() + join() |
| 停止信号 | 需要手写 flag + mutex/atomic | 内建 stop_token |
| 与 cv 集成 | 手写 flag + cv | cv.wait(lock, stop_token, pred) |
| 移动拷贝 | 移动、禁止拷贝 | 移动、禁止拷贝(同 thread) |
| 默认构造后 | joinable() == false | joinable() == false(同 thread) |
sizeof | ~8 字节 (pthread_t) | ~24 字节 (pthread_t + stop_source + ptr) |
| 典型引入 bug | 忘记 join → terminate | stop_token 不被检查 → join 阻塞 |
# 8.2 什么场景下 thread 仍有优势
场景 1:平台特定的线程属性设置
thread + native_handle → 直接调 pthread_attr_setinheritsched
jthread 的内部 thread 是私有的——拿不到 native_handle
场景 2:不需要停止信号的「执行即忘」模式
thread t = std::thread(f); t.detach();
明确知道线程会自行结束且不需要等待结果
jthread 的析构自动 join——不能用于 detach 场景
场景 3:C++17 及以下的项目
jthread 是 C++20 特性——旧项目只能用 thread
场景 4:需要精细控制 join 时机(非析构时)
thread 的显式 join 让你在任何函数中等待
jthread 的 join 只在析构时自动发生——如果中途想 join,需要额外方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 9. 常见陷阱与反模式
# 9.1 detach 后的野引用灾难
void danger() {
int local = 42;
std::thread t([&local] { // ❌ 引用捕获 local
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << local; // local 已经析构——UB
});
t.detach(); // 线程独立——但 local 马上就消失
} // local 析构
// detach 的线程在 1 秒后访问已回收的栈 → SIGSEGV 或读垃圾值
2
3
4
5
6
7
8
9
10
# 9.2 线程函数异常吞没
std::thread t([] {
throw std::runtime_error("oops"); // ❌ 线程里抛异常——不被任何人捕获
// → std::terminate! 整个进程死亡!
});
// 正确——在线程入口捕获所有异常
std::thread t([] {
try {
do_work();
} catch (...) {
// 记录日志、通知主线程
}
});
2
3
4
5
6
7
8
9
10
11
12
13
核心原则:线程的入口函数 = 独立程序。它必须自给自足——包括自己的异常处理边界。
# 9.3 忘记 join 而依赖析构——std::terminate 的死亡召唤
这个问题在第 1 章已展开——这里的重点在于检测:
// 代码审查 checklist:
std::thread t(worker);
// ... 几十行代码 ...
return; // ⚠️ 提前 return——t.joinable() 仍为 true——析构时 terminate
2
3
4
静态分析工具(clang-tidy 的 bugprone-unused-return-value 等)能捕捉部分场景——但最可靠的是:统一使用 jthread,从根本上消除这个 bug 类。
# 9.4 thread 对象被移动后仍被使用
std::thread t1(worker);
std::thread t2 = std::move(t1); // t1 变成空线程(joinable() == false)
t1.join(); // ❌ joinable() == false → std::system_error
// 规范:移动后的 thread 不应该再被使用——除了赋值和析构
2
3
4
5
# 9.5 线程 ID 复用——假死检测的陷阱
// ❌ 不可靠的「线程是否活着」检测
auto tid = t.native_handle(); // 拿到线程 ID
// ... 线程结束 ...
std::thread t2(worker2); // 可能复用同一个线程 ID!
if (pthread_kill(tid, 0) == 0) {
// 「线程还活着」——但这是 t2,不是 t!
}
2
3
4
5
6
7
8
9
正确做法:用 std::thread::joinable() 检查对象状态——不是线程 ID 存活状态。
# 10. 综合案例串讲
# 10.1 案例真相揭晓
回到第 1 章八个疑问,逐条作答:
| # | 疑问 | 答案 |
|---|---|---|
| ① | 线程什么时候开始执行? | 第 3.1:构造时立即启动——RAII 的铁律 |
| ② | 为什么析构时 joinable → terminate? | 第 3.2:最响亮的告警——告诉你有 bug |
| ③ | thread 为什么不能拷贝? | 第 3.4:pthread_t 是不可拷贝的物理实体——移动是唯一的正确语义 |
| ④ | pthread_create 和 join 的内核真相? | 第 4 章:clone + futex_wait + exit 的完整流水线 |
| ⑤ | thread_local 在子线程的初始化? | 第 5 章:线程创建时 memcpy 模板 + 首次访问动态 TLS |
| ⑥ | jthread 的自动 join 原理? | 第 6 章:析构 = request_stop + join ——线程有退出信号 + 等待 |
| ⑦ | stop_token 的原子机制? | 第 7 章:stop_state 中 atomic 状态机 + shared_ptr/weak_ptr 管理 |
| ⑧ | thread 还有什么存在意义? | 第 8.2:平台属性、C++17 兼容、detach 场景 |
案例①修复——~thread 崩溃:
// ❌ 原版
~MarketDataGateway() { disconnect(); } // 忘了 join
// ✅ 修复——用 jthread(C++20)
class MarketDataGateway {
std::jthread receiver_thread_;
void receive_loop(std::stop_token st) {
while (!st.stop_requested()) {
auto data = read_socket();
if (data) process(data);
}
}
// ~MarketDataGateway → ~jthread:
// ① request_stop → receive_loop 感知 → 退出 while
// ② join → 等待线程完整退出 ✅
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
案例②修复——jthread 停止延迟:在 process_batch 内部插入协作点:
void process_batch(std::stop_token st, const Batch& batch) {
for (auto& item : batch) {
if (st.stop_requested()) return; // ② 细粒度检查——最多等一个 item
process_item(item);
}
}
2
3
4
5
6
# 10.2 一次 thread 的完整生平——从构造到 join
std::thread t(worker, arg1, arg2);
═══════ 编译期 ═══════
语义层:
t 是 thread 类型——移动构造、禁止拷贝
sizeof(t) = 8 字节(pthread_t)
析构 = joinable() ? terminate : 普通析构
类型层:
thread 是 RAII 的线程 handle——不是线程本身
joinable() = true → 有一个活跃的线程关联
═══════ 运行期 ═══════
构造阶段 (~15μs):
① decay_copy(arg1, arg2) → tuple<Arg1, Arg2> 生命期到线程入口
② clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_THREAD, ...) → 新 task_struct
③ 新线程被加入 runqueue → 等待 CPU 调度
④ pthread_t 存入 thread 对象的 native_handle
运行阶段:
新线程:执行 thread_proxy → 解包参数 → 调用 worker(arg1, arg2)
主线程:继续执行(异步)
join 阶段 (~0-∞):
t.join() → pthread_join(tid)
→ futex_wait → 等待目标线程的 futex
→ 目标线程结束时 → futex_wake → join 返回
→ joinable() = false
析构阶段 (~0.1μs):
~thread → 检查 joinable() → false → 什么都不做
sizeof(t) 的 8 字节被回收
═══════ 性能全景(AMD 7950X) ═══════
构造 (thread creation): ~15 μs (clone + task_struct)
构造 (jthread creation): ~16 μs (同上 + stop_state 分配)
join (空线程): ~5 μs (futex 立即返回)
析构 (joinable==false): ~0.1 μs (只检查一个 bool)
request_stop: ~10 ns (一次 atomic exchange)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 10.3 设计哲学回扣
哲学 1:RAII 在并发领域——构造获取线程、析构释放线程
thread 是资源管理在并发领域的直接延伸。构造 = 创建线程(获取资源),join = 回收线程(释放资源)。RAII 不只能管理内存——它能管理任何需要「获取-释放」配对的资源。 jthread 把这条哲学推到了终点——析构自动回收、不需要程序员手动 join。
哲学 2:禁止拷贝不是限制——是物理真实的反映
你可以拷贝一个 std::string 是因为它的值是抽象的——可以复制。你不能拷贝一个 std::thread 是因为它代表了一个内核中的物理实体——一个正在执行的指令流。物理实体天然是独占的——拷贝在语义上不可能。C++ 用 = delete 表达这条物理约束——这不是类型的限制,是现实的约束。
哲学 3:协作取消 = 优雅退出 + 数据完整性——永远不要中途杀死线程
jthread 的 request_stop 不是 pthread_cancel(异步终止)——是协作信号。线程在安全点检查、在安全的时间退出——保持数据结构的一致性。这和数据库的「在线备份」、网络协议的「graceful shutdown」共享同一哲学:退出也要遵循逻辑——不是拔电源。
哲学 4:stop_token 是标准化的退出信号——把「停止」从自定义 flag 提升为类型系统的一部分
C++11 时代每个团队都有自己的「停止 flag」——atomic<bool> running_、带锁的 bool stop_、用 condition_variable 的通知。C++20 的 stop_token 把这些模式统一为一个标准类型——和 condition_variable 集成、被 jthread 自动管理。好的标准化不只提供新功能——它把分散的最佳实践收敛为类型系统的一部分。
哲学 5:thread_local 是物理隔离——用空间换零同步
每个线程有自己独立的 TLS 副本——不需要锁、不需要原子操作、不需要内存序。并发安全的最高境界不是「共享但同步」——是「不共享」。 TLS 和 jthread 的 stop_token(通过 shared_ptr 共享的控制块)形成鲜明对比——前者「各管各的」、后者「共享但原子」。两者在同一个并发系统中各司其职:TLS 管数据拷贝,stop_token 管信号通信。
# 10.4 速查表合集
thread vs jthread 速查:
| 特征 | thread | jthread |
|---|---|---|
| C++ 版本 | C++11 | C++20 |
| 析构安全 | ❌ joinable → terminate | ✅ 自动 request_stop + join |
| 停止信号 | 手写 | 内建 stop_token |
| cv 集成 | 手写 | cv.wait(lock, st, pred) |
| sizeof | ~8B | ~24B |
| detach 支持 | ✅ | ❌(析构总是 join) |
| 新项目推荐 | ❌(除非特殊需求) | ✅(默认选择) |
线程生命周期操作:
join = 阻塞等待线程结束 + 回收资源(joinable → false)
detach = 分离线程(joinable → false,线程独立)
joinable = 检查是否有关联的活跃线程
native_handle = 获取底层 pthread_t / HANDLE
request_stop (jthread only) = 请求线程停止(设置 stop_source 标志)
get_stop_token (jthread only) = 获取 stop_token 供线程内部检查
2
3
4
5
6
7
正确范式:
// ✅ thread——手动管理生命周期
std::thread t(func);
// ... 做其他事
t.join(); // 必须 join 或 detach——否则 terminate
// ✅ jthread——自动管理生命周期
std::jthread jt([](std::stop_token st) {
while (!st.stop_requested()) do_work();
});
// 析构时自动 request_stop + join ✅
// ✅ jthread + condition_variable——可中断等待
std::condition_variable_any cv;
std::mutex mtx;
std::jthread jt([&](std::stop_token st) {
std::unique_lock lock(mtx);
cv.wait(lock, st, [] { return data_ready || stop_requested; });
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
本篇小结:
thread是 C++ 并发的「手动挡」——掌控一切也负责一切。jthread是「自动挡」——帮你处理最危险的场景(析构时的线程管理)和最繁琐的细节(停止信号),同时通过stop_token保留了优雅退出的控制权。从 thread 到 jthread,不是「新特性替代旧特性」——是「10 年工程教训的最优编码」。
下一篇:线程创建和协调说了。下一篇进入 45.异步编程future家族——future/promise/packaged_task 三件套、std::async 启动策略陷阱(
std::launch::asyncvsdeferred)、shared_future 的广播语义、C++20 的std::jthread与 future 的协调模式。