线程的基本操作
C++ 中线程启动后需调用 join() 等待其结束,或用 detach() 分离线程后台运行,否则析构时报错。避免线程访问已销毁局部变量。推荐用 RAII 自动管理线程生命周期。
线程的基本操作
每个程序至少有一个线程:主线程(执行 main()
函数)。使用 std::thread
可以创建更多线程,它们与主线程并发运行。
当线程完成任务,其对应的 std::thread
对象必须被妥善处理(如 join()
或 detach()
),否则程序会异常终止。
启动线程
启动线程的基本用法
1
2
void do_some_work();
std::thread my_thread(do_some_work);
创建 std::thread
对象时即启动线程。需包含 <thread>
头文件。
使用函数对象
1
2
3
4
5
6
7
8
9
10
class background_task {
public:
void operator()() const {
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
最令人头痛的语法解析(Most Vexing Parse)
1
std::thread my_thread(background_task()); // ❌ 实际上是函数声明!
看起来像是构造一个 std::thread
对象 my_thread
,传入 background_task()
作为参数,但实际上,C++ 编译器会将它解析为函数声明,而不是对象定义。
从 C++ 编译器的视角来看,这个语法匹配了下面这种形式的函数声明:
1
2
3
返回类型 函数名 ( 参数 );
---------- -------- --------------------
std::thread my_thread (background_task(*)())
即:这是一个函数声明,函数名是 my_thread
,它的参数是一个函数指针,这个指针指向一个无参数、返回类型为 background_task
的函数,整个函数返回一个 std::thread
对象。
所以尽量避免写形如 Type name(Type())
的代码。
避免方法:
使用额外一对括号
1
std::thread my_thread((background_task())); // 使用双括号
外层括号强制编译器将其解释为构造函数参数,而不是函数声明。
使用统一初始化语法(C++11 起)
1
std::thread my_thread{background_task()}; // 使用统一初始化语法
大括号初始化不会被解释为函数声明。
使用 Lambda 表达式
1
2
3
4
std::thread my_thread([] {
do_something();
do_something_else();
});
等待线程完成(join)
线程运行后必须:
join()
:等待线程完成detach()
:分离线程,后台运行否则程序崩溃(析构时报
std::terminate()
)std::thread
的析构函数这么写的1 2 3 4 5
~thread() { if (joinable()) { std::terminate(); // 崩溃:没处理这个线程 } }
也就是说:
- 如果线程还 joinable(可连接),说明它没被处理;
- 析构时如果忘了
join()
或detach()
,就直接调用std::terminate()
。
示例:线程访问了已经销毁的局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct func {
int& i;
func(int& i_) : i(i_) {}
void operator() () {
for (unsigned j = 0; j < 1000000; ++j) {
do_something(i); // 潜在访问无效引用
}
}
};
void oops() {
int some_local_state = 0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // ❌ 没有等待线程结束
} // ❌ 线程仍运行,但局部变量销毁
- 主线程中
some_local_state
是局部变量,其生命周期到oops()
函数结束为止。 - 新线程通过引用访问它,如果未等待线程结束(即使用
detach()
),新线程可能在局部变量销毁后仍尝试访问它。 - 这是一种典型的多线程悬空引用(Dangling Reference)问题,可能导致程序崩溃或行为异常。
🧵 主线程执行步骤 | 🔀 新线程执行步骤 |
---|---|
使用 some_local_state 构造 my_func | |
创建新线程 my_thread | |
启动线程,执行 func::operator() | |
do_something(i) 使用 some_local_state 的引用 | |
调用 my_thread.detach() | |
some_local_state 被销毁 | 线程仍在运行,可能继续访问 some_local_state (已销毁) |
函数 oops() 结束 | 若线程仍执行 do_something(i) ,则访问无效引用 → ❌ 未定义行为 |
特殊情况下的等待
线程异常期间也要确保调用 join()
。否则会因为没有汇入线程导致程序崩溃。
示例:异常保护下的 join
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct func; // 同上
void f() {
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
try {
do_something_in_current_thread();
} catch (...) {
t.join(); // ① 异常时确保线程结束
throw;
}
t.join(); // ② 正常路径也等待线程结束
}
RAII风格的线程保护(推荐)
使用析构函数自动调用 join()
,避免忘记或异常跳过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class thread_guard {
std::thread& t;
public:
explicit thread_guard(std::thread& t_): t(t_) {}
~thread_guard() {
if (t.joinable()) {
t.join(); // ① 确保线程汇入
}
}
thread_guard(thread_guard const&) = delete; // ② 禁止拷贝
thread_guard& operator=(thread_guard const&) = delete;
};
void f() {
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t); // ③ RAII 保证安全
do_something_in_current_thread();
} // ④ 离开作用域自动 join
后台运行线程(detach)
detach()
表示让线程在后台运行,即成为守护线程(daemon thread):
1
2
3
std::thread t(do_background_work);
t.detach();
assert(!t.joinable()); // 已分离
使用 t.joinable()
判断线程是否可被 join。
示例:多文档编辑器中使用分离线程
1
2
3
4
5
6
7
8
9
10
11
12
13
void edit_document(std::string const& filename) {
open_document_and_display_gui(filename);
while (!done_editing()) {
user_command cmd = get_user_input();
if (cmd.type == open_new_document) {
std::string const new_name = get_filename_from_user();
std::thread t(edit_document, new_name); // ① 新线程打开新文档
t.detach(); // ② 分离线程后台运行
} else {
process_user_input(cmd);
}
}
}
用户打开新文档时,创建新线程进行处理。每个文档处理线程互不干扰,适合使用分离线程。
对比
图解
1
2
3
4
5
6
7
主线程
│
├─ 创建 std::thread t(...)
│
├─ join() ─────────────→ 等待子线程执行完毕(同步)
│
└─ detach() ───────────→ 子线程后台独立执行(异步,自己跑)
基础定义对比
功能 | join() | detach() |
---|---|---|
含义 | 主动等待线程执行完毕 | 分离线程,让其独立后台运行 |
所属权 | 调用后线程归主线程管理 | 调用后线程归系统管理 |
阻塞行为 | ✅ 阻塞当前线程,直到目标线程执行完 | ❌ 不阻塞,立即返回 |
清理资源 | ✅ 自动释放线程资源 | ✅ 系统在线程结束时自动清理 |
是否 joinable() | ❌ join 后不可 joinable | ❌ detach 后不可 joinable |
使用场景对比
场景 | 推荐操作 | 理由 |
---|---|---|
需要等待线程结果 | join() | 等待线程执行完成,例如:下载完成、计算完成 |
子线程非关键、后台运行 | detach() | 无需等待线程,例如:日志、心跳、监控、缓存清理 |
创建线程但忘记处理 | ❌ 程序崩溃 | 如果既不 join 也不 detach ,析构时会 std::terminate() |
注意事项对比
项目 | join() | detach() |
---|---|---|
是否可以多次调用 | ❌ 不可以多次 join | ❌ 不可以多次 detach |
是否可以混用 | ❌ join 和 detach 只能调用一次(或不调用) | 否则程序崩溃 |
销毁前是否必须调用 | ✅ 必须调用(join 或 detach 至少一个) | ✅ 必须调用(否则析构崩溃) |
线程安全性 | 安全,资源回收明确 | ⚠️ 危险,需确保线程中不访问悬空对象 |
调试友好性 | ✅ 调试方便,主线程可捕获异常或日志 | ❌ 调试困难,后台线程奔溃你可能都不知道 |
总结
操作 | 含义与场景 |
---|---|
join() | 主动等待线程完成,释放资源,常规推荐方式 |
detach() | 分离线程,后台运行,适用于 fire-and-forget |
RAII | 用类封装线程,自动在析构时调用 join() |
避免错误 | 不要在线程中访问已销毁的局部变量引用 |