编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
        • 1. 案例引入
          • 1.1 ~thread 崩溃——join 或 detach 必须选一个
          • 1.2 jthread协作取消丢失
          • 1.3 八个待解疑问
        • 2. 架构概览
          • 2.1 thread 和 jthread 的三层模型
          • 2.2 为何这么切
        • 3. std::thread 的核心语义
          • 3.1 构造:移动不可拷贝与线程立即启动
          • 3.2 析构:joinable → std::terminate 的硬规则
          • 3.3 join vs detach 的生死边界
          • 3.4 为什么 thread 不能拷贝——所有权与物理实体相等
        • 4. thread 的 pthread 底层映射
          • 4.1 从 thread 构造到 pthread_create 的全链路
          • 4.2 join 如何等待——pthread_join 的内核机制
          • 4.3 native_handle 的用途与安全边界
          • 4.4 线程创建失败的三种原因与检测
        • 5. thread_local 在子线程中的初始化时机
          • 5.1 TLS 段的 Lazy 初始化模型
          • 5.2 什么时候可能踩到未初始化的 TLS
          • 5.3 与全局静态对象的线程安全对比
        • 6. jthread 的 join 自动保证
          • 6.1 析构函数自动 request_stop + join
          • 6.2 为什么这是 C++20 最安全的线程类型
          • 6.3 从 thread 到 jthread 的最小迁移路径
        • 7. stop_token 协作取消机制——深层原理
          • 7.1 三组件:stopsource / stoptoken / stop_callback
          • 7.2 内部共享状态的原子语义
          • 7.3 stop_callback 的注册与析构顺序
          • 7.4 与 condition_variable 的集成——可中断等待
          • 7.5 为什么 stop 是不可逆的——设计意图
        • 8. jthread 与 thread 的完整对决
          • 8.1 七维全量对比表
          • 8.2 什么场景下 thread 仍有优势
        • 9. 常见陷阱与反模式
          • 9.1 detach 后的野引用灾难
          • 9.2 线程函数异常吞没
          • 9.3 忘记 join 而依赖析构——std::terminate 的死亡召唤
          • 9.4 thread 对象被移动后仍被使用
          • 9.5 线程 ID 复用——假死检测的陷阱
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次 thread 的完整生平——从构造到 join
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

thread与jthread机制

# 44.thread与jthread机制

# 目录介绍

  • 1. 案例引入
    • 1.1 ~thread 崩溃——join 或 detach 必须选一个
    • 1.2 jthread 的协作取消丢失——为什么线程停不下来
    • 1.3 八个待解疑问
  • 2. 架构概览
    • 2.1 thread 和 jthread 的三层模型
    • 2.2 为何这么切
  • 3. std::thread 的核心语义
    • 3.1 构造:移动不可拷贝与线程立即启动
    • 3.2 析构:joinable → std::terminate 的硬规则
    • 3.3 join vs detach 的生死边界
    • 3.4 为什么 thread 不能拷贝——所有权与物理实体相等
  • 4. thread 的 pthread 底层映射
    • 4.1 从 thread 构造到 pthread_create 的全链路
    • 4.2 join 如何等待——pthread_join 的内核机制
    • 4.3 native_handle 的用途与安全边界
    • 4.4 线程创建失败的三种原因与检测
  • 5. thread_local 在子线程中的初始化时机
    • 5.1 TLS 段的 Lazy 初始化模型
    • 5.2 什么时候可能踩到未初始化的 TLS
    • 5.3 与全局静态对象的线程安全对比
  • 6. jthread 的 join 自动保证
    • 6.1 析构函数自动 request_stop + join
    • 6.2 为什么这是 C++20 最安全的线程类型
    • 6.3 从 thread 到 jthread 的最小迁移路径
  • 7. stop_token 协作取消机制——深层原理
    • 7.1 三组件:stop_source / stop_token / stop_callback
    • 7.2 内部共享状态的原子语义
    • 7.3 stop_callback 的注册与析构顺序
    • 7.4 与 condition_variable 的集成——可中断等待
    • 7.5 为什么 stop 是不可逆的——设计意图
  • 8. jthread 与 thread 的完整对决
    • 8.1 七维全量对比表
    • 8.2 什么场景下 thread 仍有优势
  • 9. 常见陷阱与反模式
    • 9.1 detach 后的野引用灾难
    • 9.2 线程函数异常吞没
    • 9.3 忘记 join 而依赖析构——std::terminate 的死亡召唤
    • 9.4 thread 对象被移动后仍被使用
    • 9.5 线程 ID 复用——假死检测的陷阱
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次 thread 的完整生平——从构造到 join
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 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
};
1
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 → 整个进程炸掉
        → 不仅是这个模块——整个交易引擎崩溃!
1
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 秒!
}
1
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 章
1
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 时的等待语义)               │
└──────────────────────────────────────────────────────────┘
1
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 年的经验教训编码进类型系统。
1
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 拥有线程
1
2
3
4
5
6
7

为什么线程构造 = 立即启动? 不采用「两阶段构造」(先构造对象、再 start()):

C++ 的设计选择——RAII 的铁律:
  构造 = 获取资源。线程资源在构造时获取——线程立即运行。

如果允许延迟启动:
  auto t = std::thread(func);  // 构造——但线程还没跑?
  // ... 中间可能抛异常
  t.start();                   // 启动了但可能永远不会调用?
  → 如果中间抛异常——t 析构时的状态无法确定(跑了还是没跑?)
1
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...)
}
1
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:「这个参数我用引用传递——我知道自己在干什么」
}
1
2
3
4
5
6
7
8

# 3.2 析构:joinable → std::terminate 的硬规则

~thread() {
    if (joinable()) {
        std::terminate();   // ⚠️ 不是「忘了 join」——是「严重逻辑错误」
    }
}
1
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
1
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
         线程成为「守护线程」——在进程结束时被操作系统回收
1
2
3
4
5
6

join 的汇编本质:

std::thread t(worker);
t.join();
1
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
1
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 了?无法定义一致语义。
1
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 */ });
1

这条语句在 Linux (glibc) 上的完整调用链:

用户层:
  std::thread::thread(Callable&amp;&amp; f)
    → 创建 __thread_data 对象
    → decay_copy 参数打包进 tuple
    → 包装成调用适配器

标准库层(libstdc++):
  __gthread_create(native_handle, thread_proxy, data_ptr)
    → 创建线程属性:PTHREAD_CREATE_DETACHED = 0 (joinable)
    → pthread_create(&amp;tid, &amp;attr, &amp;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 调度
1
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 微秒——远大于锁的开销。
1
2
3
4
5

# 4.2 join 如何等待——pthread_join 的内核机制

t.join();
// → pthread_join(t.native_handle(), nullptr)
1
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 状态清除
1
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 被唤醒
1
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
                            //    析构函数可能不会被调用!
1
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';
    }
}
1
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 块、调用构造
  → 后续访问→ 直接返回已缓存的地址
  → 开销:首次访问有分配+构造成本
1
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 在线程创建时初始化——没问题
}
1
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 是安全的
}
1
2
3
4
5
6
7
8
9
10

# 5.3 与全局静态对象的线程安全对比

全局 static 对象(单份):
  初始化在多线程环境下被 C++11 编译器保护(双重检查锁+futex)
  每个线程访问同一份对象——不需要 TLS

thread_local 对象(每线程一份):
  初始化在线程创建时——同一时刻只有一个线程在跑(创建它的线程)
  → 不需要锁!
  子线程访问自己的副本——也不需要锁!
  → TLS = 编译器的物理隔离 = 天然的线程安全
1
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 风险
};
1
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 析构完成
1
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 的崩溃完全不同。
1
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 就是标准化的退出信号
};
1
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            │
└─────────────────┘      └─────────────────┘      └──────────────────────┘
1
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;
    }
};
1
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&lt;stop_state>  —— 拥有所有权
stop_token  持有:weak_ptr&lt;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 篇中的语义完全一致)
1
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 的线程中被调用——不是在被停止的线程
1
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; });
}
1
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() → 任一为真 → 返回
1
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 时被立即调用——无法回滚
1
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,需要额外方法
1
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 或读垃圾值
1
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 (...) {
        // 记录日志、通知主线程
    }
});
1
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
1
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 不应该再被使用——除了赋值和析构
1
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!
}
1
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 → 等待线程完整退出 ✅
};
1
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);
    }
}
1
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&lt;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)
1
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 供线程内部检查
1
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; });
});
1
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::async vs deferred)、shared_future 的广播语义、C++20 的 std::jthread 与 future 的协调模式。

上次更新: 2026/06/10, 11:13:41
mutex与条件变量
异步编程future家族

← mutex与条件变量 异步编程future家族→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式