这本书主要讲的是c++多线程编程相关的内容,第二章主要讲的是std:thread的使用注意事项

线程的启动

在使用std:thread时,我们要注意的一点是c++中的语法解析,原文是(c++’s most vexing parse).

函数对象(Functors)即任何重载了 operator() 的类的实例。Lambda 表达式是匿名函数对象的语法糖

如果将函数对象作为thread的构造函数参数而不是普通的函数对象,就注意不要使用临时变量

1
2
3
4
5
6
7
8
9
struct fun {
void operator()() {
std::cout << "MyFunctor is running in a thread!" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "MyFunctor finished." << std::endl;
}
};
std::thread my_thread(fun());
这样的写法,很不幸会被识别为,thread类型返回值的函数声明

解决这样的情况,可以用{}进行初始化,或者用额外的括号,以及使用命名变量而不是临时变量。

同样的,lambda函数也是解决这样的情况的一个不错的方法

线程的等待

众所周知,线程有两种结束的方法,detach和join,join会阻塞主线程,直到线程运行完毕,detach则会将资源交给操作系统,不再关心被detach的线程如何结束。这里就会出现访问已释放的资源的风险。

这里要注意子线程的参数如果是函数对象的话,不能访问主线程中的变量的引用(如果要访问,一定要注意生命周期),因为detach之后主线程可能先于子线程结束,这样就会出现未定义行为。举个例子,就是lambda函数[&]{}捕获引用后又作为函数对象传递给thread

特殊情况下的等待:这里主要是提醒在使用try catch的时候,如果在try catch之后有t.join()那么在catch的地方throw之前一定也要t.join()

线程中的参数传递(重要)

thread有一个毛病,就是当你传递参数给thread的构造函数时,thread会触发拷贝构造函数,将参数拷贝到新线程的内存空间中(如果使用一个可调用的对象(也就是函数对象)作为线程函数的参数也会先复制到新线程的存储空间中,然后在新线程中调用执行函数对象)

即使你的thread的函数的参数要求是引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func(int& x) {
x += 10;
std::cout << "In thread, x = " << x << std::endl;
}

int main() {
int a = 5;

std::thread t(func, a); // 👈 看似传了引用,实际是复制!
t.join();

std::cout << "After thread, a = " << a << std::endl;
return 0;
}

如果实在想要穿引用,就要用std::ref();

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

void func(int& x) {
x += 10;
std::cout << "In thread, x = " << x << std::endl;
}

int main() {
int a = 5;

std::thread t(func, std::ref(a)); // ✅ 使用 std::ref 显式传引用
t.join();

std::cout << "After thread, a = " << a << std::endl;
return 0;
}

为什么要默认复制?

这是 出于安全和生命周期管理的考虑

  • 新线程可能会比调用线程存活更久
    如果直接引用主线程的局部变量,而主线程结束了,那么引用就悬空了(dangling reference),会造成未定义行为。

例子:

1
2
3
4
5
6
7
8
9
void func(int& x) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << x << std::endl; // 可能访问已销毁的变量!
}

int main() {
std::thread t(func, std::ref(42)); // ❌ 错误!引用临时对象
t.detach();
}

为了防止这种情况,C++ 设计 std::thread默认复制所有参数,以确保新线程使用自己的独立副本,不会引用无效内存。

传递参数时,容易出现一些错误行为

1
2
3
4
5
6
7
void f(int i,std::string const& s);
void oops(int param)
{
char buffer[1024];
std::thread(f,3,buffer);//为避免悬空指针,应使用std::string显式转换
t.detach();
}

这里会出现一个问题,函数f需要一个string类型的参数,我们提供的是buffer,事实上是一个指针,正常情况下编译器会将其隐式转换为string,但是thread的拷贝构造函数也会将参数复制一份,如果此时还没有进行隐式转换,而thread已经完成了复制,那么隐式转换完毕的buffer也不再有机会传递到新的线程中,从而导致f函数接收到错误的参数类型。

为了避免这样的情况,我们需要使用std::thread(f,3,std::string(buffer);

接下来的部分,原文我个人感觉有一定错误,暂且跳过去

如果我们想要将类的成员函数作为thread的参数函数,具体做法如下

1
2
3
4
5
6
7
8
class X
{
public:
void do_soming(int s);
};
X my_x;
int num =0;
std::thread t(&X::do_soming,&my_x,num);

这里传入实例的指针起到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 的设计目标就是:

  1. 自动管理线程生命周期(RAII)
  2. 简化中断(stop)机制
  3. 提高可读性和安全性

⚙️ 三、基本用法示例

✅ 自动 join 的线程

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

void work() {
std::cout << "Worker thread running\n";
}

int main() {
std::jthread t(work); // 创建并启动线程
// 不需要显式调用 t.join()
std::cout << "Main thread exiting\n";
} // 离开作用域时,jthread 自动 join

输出:

1
2
Worker thread running
Main thread exiting

🔹 当 t 离开作用域时,它的析构函数会自动调用 .join(),确保线程结束后再销毁。
这就彻底解决了 std::thread 常见的忘记 join() 导致崩溃的问题。


🧩 四、可中断的线程(Stop Token 机制)

std::jthread 新增了一个 中断机制(stop token),可以让你优雅地通知线程“停止工作”。

1️⃣ 基本示例

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

void worker(std::stop_token st) {
while (!st.stop_requested()) { // 检查是否收到停止请求
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "Worker stopped.\n";
}

int main() {
std::jthread t(worker); // worker 会自动接收 stop_token
std::this_thread::sleep_for(std::chrono::seconds(2));
t.request_stop(); // 请求停止线程
}

输出示例:

1
2
3
4
Working...
Working...
Working...
Worker stopped.

🔹 std::jthread 在启动时会自动为线程提供一个 std::stop_token 参数,
只要你的线程函数声明第一个参数为 std::stop_tokenstd::jthread 会自动传入。

🔹 当你调用 t.request_stop() 时,

  • 线程中的 stop_token 会变为已请求状态;
  • 线程循环中可以通过 st.stop_requested() 检查并退出。

2️⃣ Stop Token 工作原理

std::jthread 内部持有一个 std::stop_source 对象,线程函数通过 std::stop_token 获取状态。

它们的关系如下:

1
2
3
4
5
6
7
8
9
┌─────────────────────┐
│ jthread t │
│ ├─ stop_source │◀─────────────┐
│ └─ thread handle │ │
└─────────────────────┘ │

┌──────────────┘

stop_token(传给线程函数)

t.request_stop() 被调用时:

  • stop_source 标记为已停止;
  • 所有通过该 token 获取的线程都能检测到 stop_requested() 返回 true
  • 从而线程函数可以优雅地结束。

3️⃣ Stop Callback(注册回调函数)

你还可以注册一个回调,当停止请求发生时自动执行:

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

void worker(std::stop_token st) {
std::stop_callback cb(st, [] {
std::cout << "Stop requested! Cleaning up...\n";
});

while (!st.stop_requested()) {
std::cout << "Running...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
}

int main() {
std::jthread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop(); // 会触发回调
}

输出:

1
2
3
4
Running...
Running...
Running...
Stop requested! Cleaning up...

🧮 五、构造和移动

std::jthreadstd::thread 一样:

  • 不可复制(=delete
  • 但可以移动

✅ 移动示例

1
2
std::jthread t1([] { std::cout << "Thread 1\n"; });
std::jthread t2 = std::move(t1); // 所有权转移

移动后:

  • t1 变为空线程;
  • t2 接管原先的执行任务。

🔧 六、典型应用场景

场景 std::thread std::jthread 优势
启动线程执行任务 ✅ 自动 join,安全性更高
周期性工作线程 ✅ 支持 stop_token,易于终止
线程池或任务调度 ⚠️ 需自管停止标志 ✅ 内置可中断机制
异常路径安全退出 ❌ 需显式 join/detach ✅ RAII 自动清理

⚠️ 七、注意事项

  1. std::jthread 析构时 自动 join(阻塞等待线程结束),
    如果线程还在执行,主线程会阻塞,直到线程退出。

  2. 如果你不希望阻塞,可以:

    • 调用 t.detach()
    • 或提前调用 t.request_stop() 并等待线程结束。
  3. std::jthread 的中断机制是 协作式的
    它不会强制中止线程,只能通知线程 “该停了”
    线程函数需要自己定期检查 stop_token


🧩 八、完整示例:优雅停止的后台任务

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

void background_task(std::stop_token st) {
while (!st.stop_requested()) {
std::cout << "Doing background work...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "Gracefully stopping background work.\n";
}

int main() {
std::jthread worker(background_task);
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Requesting stop...\n";
worker.request_stop(); // 优雅地请求停止
} // 离开作用域自动 join

输出:

1
2
3
4
5
Doing background work...
Doing background work...
Doing background work...
Requesting stop...
Gracefully stopping 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之后就再也无法恢复了