《C++ Concurrency in Action》第二章
这本书主要讲的是c++多线程编程相关的内容,第二章主要讲的是std:thread的使用注意事项
线程的启动
在使用std:thread时,我们要注意的一点是c++中的语法解析,原文是(c++’s most vexing parse).
函数对象(Functors)即任何重载了 operator() 的类的实例。Lambda 表达式是匿名函数对象的语法糖
如果将函数对象作为thread的构造函数参数而不是普通的函数对象,就注意不要使用临时变量
1 | struct fun { |
解决这样的情况,可以用{}进行初始化,或者用额外的括号,以及使用命名变量而不是临时变量。
同样的,lambda函数也是解决这样的情况的一个不错的方法
线程的等待
众所周知,线程有两种结束的方法,detach和join,join会阻塞主线程,直到线程运行完毕,detach则会将资源交给操作系统,不再关心被detach的线程如何结束。这里就会出现访问已释放的资源的风险。
这里要注意子线程的参数如果是函数对象的话,不能访问主线程中的变量的引用(如果要访问,一定要注意生命周期),因为detach之后主线程可能先于子线程结束,这样就会出现未定义行为。举个例子,就是lambda函数[&]{}捕获引用后又作为函数对象传递给thread
特殊情况下的等待:这里主要是提醒在使用try catch的时候,如果在try catch之后有t.join()那么在catch的地方throw之前一定也要t.join()
线程中的参数传递(重要)
thread有一个毛病,就是当你传递参数给thread的构造函数时,thread会触发拷贝构造函数,将参数拷贝到新线程的内存空间中(如果使用一个可调用的对象(也就是函数对象)作为线程函数的参数也会先复制到新线程的存储空间中,然后在新线程中调用执行函数对象)
即使你的thread的函数的参数要求是引用
1 | void func(int& x) { |
如果实在想要穿引用,就要用std::ref();
1 | #include <iostream> |
为什么要默认复制?
这是 出于安全和生命周期管理的考虑:
- 新线程可能会比调用线程存活更久。
如果直接引用主线程的局部变量,而主线程结束了,那么引用就悬空了(dangling reference),会造成未定义行为。
例子:
1 | void func(int& x) { |
为了防止这种情况,C++ 设计 std::thread 时 默认复制所有参数,以确保新线程使用自己的独立副本,不会引用无效内存。
传递参数时,容易出现一些错误行为
1 | void f(int i,std::string const& s); |
这里会出现一个问题,函数f需要一个string类型的参数,我们提供的是buffer,事实上是一个指针,正常情况下编译器会将其隐式转换为string,但是thread的拷贝构造函数也会将参数复制一份,如果此时还没有进行隐式转换,而thread已经完成了复制,那么隐式转换完毕的buffer也不再有机会传递到新的线程中,从而导致f函数接收到错误的参数类型。
为了避免这样的情况,我们需要使用std::thread(f,3,std::string(buffer);
接下来的部分,原文我个人感觉有一定错误,暂且跳过去
如果我们想要将类的成员函数作为thread的参数函数,具体做法如下
1 | class X |
这里传入实例的指针起到this指针的作用
同时,thread对于参数的传递也有一部分优化
对于参数,如果是左值,会默认调用拷贝构造函数,如果是右值,就默认调用移动构造函数
转移所有权
thread和线程本身是分离的,类似指针和资源的关系,而thread和unique_ptr一样都是可移动但不可复制的
但我们不能通过赋新值的方式来丢弃一个线程
同时可以用move的形式将thread作为参数进行传递
接下来笔者补充介绍一下c++20中引入的jthread,而本书出来时c++标准只更新到c++17,jthread还只存在于构想中
**std::jthread**(”joining thread”)是 C++20 新增的线程类,是对 std::thread 的一种 改进版。
它在易用性、安全性和可中断性方面都比 std::thread 更优雅、更安全。
我们可以把它理解为:
std::jthread = std::thread + 自动 join + 可中断机制(stop token)
🚀 一、std::jthread 的核心特点
| 特性 | std::thread |
std::jthread |
|---|---|---|
是否自动 join |
❌ 需要手动调用 .join() 或 .detach() |
✅ 析构时自动 join |
| 可否被复制 | ❌ 不可复制(只能 move) | ❌ 不可复制(只能 move) |
| 是否支持中断(停止请求) | ❌ 不支持 | ✅ 支持(stop_token) |
| 安全性 | 需要手动管理生命周期 | 自动管理,防止“悬空线程” |
| 标准版本 | C++11 | C++20 |
🧠 二、为什么要有 std::jthread
在 std::thread 中,如果你忘记在对象销毁前调用 .join() 或 .detach(),程序会直接调用 std::terminate() 崩溃。
这在大型程序或异常路径中非常危险。
std::jthread 的设计目标就是:
- 自动管理线程生命周期(RAII)
- 简化中断(stop)机制
- 提高可读性和安全性
⚙️ 三、基本用法示例
✅ 自动 join 的线程
1 |
|
输出:
1 | Worker thread running |
🔹 当 t 离开作用域时,它的析构函数会自动调用 .join(),确保线程结束后再销毁。
这就彻底解决了 std::thread 常见的忘记 join() 导致崩溃的问题。
🧩 四、可中断的线程(Stop Token 机制)
std::jthread 新增了一个 中断机制(stop token),可以让你优雅地通知线程“停止工作”。
1️⃣ 基本示例
1 |
|
输出示例:
1 | Working... |
🔹 std::jthread 在启动时会自动为线程提供一个 std::stop_token 参数,
只要你的线程函数声明第一个参数为 std::stop_token,std::jthread 会自动传入。
🔹 当你调用 t.request_stop() 时,
- 线程中的
stop_token会变为已请求状态; - 线程循环中可以通过
st.stop_requested()检查并退出。
2️⃣ Stop Token 工作原理
std::jthread 内部持有一个 std::stop_source 对象,线程函数通过 std::stop_token 获取状态。
它们的关系如下:
1 | ┌─────────────────────┐ |
当 t.request_stop() 被调用时:
stop_source标记为已停止;- 所有通过该 token 获取的线程都能检测到
stop_requested()返回true; - 从而线程函数可以优雅地结束。
3️⃣ Stop Callback(注册回调函数)
你还可以注册一个回调,当停止请求发生时自动执行:
1 |
|
输出:
1 | Running... |
🧮 五、构造和移动
std::jthread 和 std::thread 一样:
- 不可复制(
=delete) - 但可以移动
✅ 移动示例
1 | std::jthread t1([] { std::cout << "Thread 1\n"; }); |
移动后:
t1变为空线程;t2接管原先的执行任务。
🔧 六、典型应用场景
| 场景 | std::thread |
std::jthread 优势 |
|---|---|---|
| 启动线程执行任务 | ✅ | ✅ 自动 join,安全性更高 |
| 周期性工作线程 | ✅ | ✅ 支持 stop_token,易于终止 |
| 线程池或任务调度 | ⚠️ 需自管停止标志 | ✅ 内置可中断机制 |
| 异常路径安全退出 | ❌ 需显式 join/detach | ✅ RAII 自动清理 |
⚠️ 七、注意事项
std::jthread析构时 自动 join(阻塞等待线程结束),
如果线程还在执行,主线程会阻塞,直到线程退出。如果你不希望阻塞,可以:
- 调用
t.detach(); - 或提前调用
t.request_stop()并等待线程结束。
- 调用
std::jthread的中断机制是 协作式的,
它不会强制中止线程,只能通知线程 “该停了”,
线程函数需要自己定期检查stop_token。
🧩 八、完整示例:优雅停止的后台任务
1 |
|
输出:
1 | Doing background work... |
✅ 九、总结对比表
| 功能 | std::thread |
std::jthread |
|---|---|---|
| 析构时自动 join | ❌ 否 | ✅ 是 |
| 可请求停止 | ❌ 否 | ✅ 是(stop_token) |
| RAII 管理 | ⚠️ 手动 join/detach | ✅ 自动 |
| 安全性 | 需谨慎管理 | 安全,不易出错 |
| 中断控制 | 外部变量标志 | 内建 stop 机制 |
| 推荐场景 | 性能极端要求时 | 绝大多数实际应用 |
虽然 detach 后还能调用 request_stop(),但要理解一些边界行为:
1️⃣ 如果线程已经退出,request_stop() 只是无效操作
std::stop_source::request_stop()是幂等的;- 如果线程已经结束,它什么也不会做;
- 不会抛异常,不会报错。
也就是说,你可以安全地在任何时候调用 request_stop(),只不过可能不会再影响任何线程。
虽然这里jthread有点像协程,但是完全不一样,jthread stop之后就再也无法恢复了




