Chapter 2 归纳总结 --- 线程管理
来源:simonhancrew.github.io/CppConcuren... (对应《C++ Concurrency in Action》第 2 章 "Managing threads") 第 1 章讲了"为什么用并发",本章讲怎么把线程启动起来、管理好 : 构造即启动 → 等待(join)还是分离(detach)→ 传参 → 所有权移交 → 线程数量 → 线程标识。 最大的坑是局部变量的生命周期:线程比你函数活得久的烂主意(root of all evils)。
结构:2.1 线程基本操作 → 2.2 传递参数 → 2.3 转移所有权 → 2.4 确定线程数量 → 2.5 线程标识 → 2.6 本章总结。
1. 启动线程(2.1)
包含 <thread>,构造 std::thread 对象即启动(任务函数执行完线程结束)。
cpp
void do_some_work();
std::thread my_thread(do_some_work); // 传函数名
函数对象实例也可以(论上会复制到新线程的存储空间,在其中执行):
cpp
class background_task {
public:
void operator()() const { do_something(); do_something_else(); }
};
background_task f;
std::thread my_thread(f);
★ 避掉 "最令人头痛的语法解析" (Most Vexing Parse)
cpp
std::thread my_thread(background_task()); // 被解析为"函数声明",不是对象!
std::thread my_thread(background_task()); // 改多组括号 → 变成对象
std::thread my_thread{background_task()}; // 用 {} 统一初始化(C++11)
std::thread my_thread([]( ) { // 或直接 lambda
do_something(); do_something_else(); });
危险案例(清单 2.1):线程访问已销毁的局部变量
cpp
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(); // ② 或 join 与否未定
} // ③ 若 detach,新线程可能还在跑,引用了已销毁的栈变量 → UB
核心:用访问局部变量的函数开线程 = 坏主意。 常规做法是把数据复制进线程 (按值)。 只要
std::thread对象销毁前没调用join或detach,析构函数会调用std::terminate()终止程序。
2. 等待线程:join()(2.1.2)
join():阻塞当前线程,等子线程结束,并回收线程相关资源。- 注意 join 的位置 :如果
join()之前抛出异常而跳过它,子线程还没回收 → 危险。
清单 2.2 try/catch 兜底 join
cpp
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(); // ① 异常路径也 join
throw;
}
t.join(); // ② 正常路径 join
}
清单 2.3 RAII 线程守卫(重点,推荐范式)
cpp
class thread_guard {
std::thread& t;
public:
explicit thread_guard(std::thread& t_) : t(t_) {}
~thread_guard() {
if(t.joinable()) // ① 先判断是否还能 join
t.join(); // ② 析构时自动 join
}
thread_guard(const thread_guard&) = delete; // ③ 防拷贝
thread_guard& operator=(const thread_guard&) = delete;
};
void f() {
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t); // RAII:出了作用域自动 join
do_something_in_current_thread();
} // ④ 局部对象逆序销毁,先销毁 g → join 线程
RAII 精粹:把
join放进析构 +joinable()判空 + 拷贝构造拷贝赋值=delete,异常也安全。
3. 后台运行:detach()(2.1.3)
- 分离线程(daemon/守护线程) :后台独立运行,"发后即忘"(fire-and-forget)。分离后不可再 join。
- C++ 运行库保证线程退出时资源自动回收。
- 调用前必须确认
joinable()(对无执行线程 的std::thread调detach是错的)。
cpp
std::thread t(do_background_work);
t.detach();
assert(!t.joinable()); // 分离后不可 join
清单 2.4:用户编辑器/文档处理(分离线程处理任务)
cpp
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);
}
}
}
分离线程与主线程无法直接交互;数据要安全地共享(见第 3 章)。
4. 向线程函数传递参数(2.2)
传参基本机制
- 参数作为
std::thread构造函数的附加参数,拷贝到新线程内存空间。 - 即使函数参数是
引用,拷贝也会执行------ 拷贝发生在构造线程时,不是函数调用时(隐患见下)。
cpp
void f(int i, std::string const& s);
std::thread t(f, 3, "hello"); // 传 3 和字符串
悬空指针陷阱(关键)
cpp
void oops(int some_param) {
char buffer[1024]; // ① 栈上数组
sprintf(buffer, "%i", some_param);
std::thread t(f, 3, buffer); // ② 隐式 char* → 拷贝顺序不确定
t.detach(); // 线程可能在 buffer 销毁后还没完成隐式转换 → 悬空
}
COP与函数内隐式转换的顺序无法保证。 解决:显式转成
std::string再传:
cpp
std::thread t(f, 3, std::string(buffer)); // 值传递一个完整的 string
传引用:std::ref
直接传非常量引用变量会编译错(constructor 拷贝后以右值传参);用 std::ref 包住:
cpp
void update_data_for_widget(widget_id w, widget_data& data);
std::thread t(update_data_for_widget, w, std::ref(data));
成员函数作线程函数(同 std::bind 语法)
cpp
class X { public: void do_lengthy_work(int); };
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num); // 成员函数指针 + 对象指针
移动语义(只支持移动的类型)
cpp
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p)); // 必须显式 move
5. 转移所有权 std::thread(2.3)
std::thread可移动、不可拷贝 (像unique_ptr)。- 同一时刻一个实例只关联一个执行线程;移动后原对象变空(不能再 join/detach)。
cpp
std::thread t1(func1);
std::thread t2 = std::move(t1); // t1 所有权移到 t2,t1 空
t1 = std::thread(func2); // 临时线程直接赋值(移动赋值,若 t1 可 join 会先 join,否则 terminate)
6. 确定线程数量 hardware_concurrency()(2.4)
cpp
unsigned n = std::thread::hardware_concurrency(); // 硬件支持的并发线程数(核数),0 = 不可知
// 为 0 时要回退策略,比如自行选择默认值
- 并行算法常只启动并行部分所需 的线程,通常不要多于核心数。过量线程 → 上下文切换过多 → 更慢(呼应 1.2.3)。
7. 线程标识 thread::id(2.5)
- 类型
std::thread::id,可由std::thread成员get_id()或std::this_thread::get_id()获取。 - 无执行线程对象的 id 为默认构造的空 id,可判等。
std::thread::id支持比较 与哈希,可作 map 的键 / 用于检测是否为主线程。
cpp
std::thread::id main_id = std::this_thread::get_id();
std::thread::id t_id = t.get_id();
std::hash<std::thread::id> h;
if (t_id == main_id) { /* 我在主线程 */ }
8. 本章总结
| 主题 | 关键结论 |
|---|---|
| 启动 | std::thread t(f, args...),构造即启动 |
| join | 等待线程结束;只能一次;异常安全 用 RAII thread_guard |
| detach | 后台"发后即忘";先确认 joinable |
| 传参 | 默认拷贝;引用用 std::ref;const* 明转 string 防悬空;move 传所有权 |
| 所有权 | 可 move 不可 copy,RAII/所有权移交 |
| 线程数 | hardware_concurrency(),0 需回退 |
| 标识 | thread::id 可比、可哈希 |
> thread_guard 是该书第一个反"dangling"的 RAII 组件,也是后续 unique_lock 的"前奏"。
与后续章节的衔接
- 第 3 章:保护共享数据(mutex)------线程生命周期管理之后,轮到数据安全;
- 第 4 章:同步操作(CV / future)---把本章的 join 升级为"等一个计算结果/事件";
- 第 5 章:内存模型/原子 --- 真正揭示 thread 同步机制如何工作。
参考:仓库 doc/cia_ch1_concurrency_intro.md、doc/cia_ch4_sync_operations.md、doc/cia_ch5_atomic_summary.md。# Chapter 2 归纳总结 --- 线程管理
来源:simonhancrew.github.io/CppConcuren... (对应《C++ Concurrency in Action》第 2 章 "Managing threads") 第 1 章讲了"为什么用并发",本章讲怎么把线程启动起来、管理好 : 构造即启动 → 等待(join)还是分离(detach)→ 传参 → 所有权移交 → 线程数量 → 线程标识。 最大的坑是局部变量的生命周期:线程比你函数活得久的烂主意(root of all evils)。
结构:2.1 线程基本操作 → 2.2 传递参数 → 2.3 转移所有权 → 2.4 确定线程数量 → 2.5 线程标识 → 2.6 本章总结。
1. 启动线程(2.1)
包含 <thread>,构造 std::thread 对象即启动(任务函数执行完线程结束)。
cpp
void do_some_work();
std::thread my_thread(do_some_work); // 传函数名
函数对象实例也可以(论上会复制到新线程的存储空间,在其中执行):
cpp
class background_task {
public:
void operator()() const { do_something(); do_something_else(); }
};
background_task f;
std::thread my_thread(f);
★ 避掉 "最令人头痛的语法解析" (Most Vexing Parse)
cpp
std::thread my_thread(background_task()); // 被解析为"函数声明",不是对象!
std::thread my_thread(background_task()); // 改多组括号 → 变成对象
std::thread my_thread{background_task()}; // 用 {} 统一初始化(C++11)
std::thread my_thread([]( ) { // 或直接 lambda
do_something(); do_something_else(); });
危险案例(清单 2.1):线程访问已销毁的局部变量
cpp
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(); // ② 或 join 与否未定
} // ③ 若 detach,新线程可能还在跑,引用了已销毁的栈变量 → UB
核心:用访问局部变量的函数开线程 = 坏主意。 常规做法是把数据复制进线程 (按值)。 只要
std::thread对象销毁前没调用join或detach,析构函数会调用std::terminate()终止程序。
2. 等待线程:join()(2.1.2)
join():阻塞当前线程,等子线程结束,并回收线程相关资源。- 注意 join 的位置 :如果
join()之前抛出异常而跳过它,子线程还没回收 → 危险。
清单 2.2 try/catch 兜底 join
cpp
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(); // ① 异常路径也 join
throw;
}
t.join(); // ② 正常路径 join
}
清单 2.3 RAII 线程守卫(重点,推荐范式)
cpp
class thread_guard {
std::thread& t;
public:
explicit thread_guard(std::thread& t_) : t(t_) {}
~thread_guard() {
if(t.joinable()) // ① 先判断是否还能 join
t.join(); // ② 析构时自动 join
}
thread_guard(const thread_guard&) = delete; // ③ 防拷贝
thread_guard& operator=(const thread_guard&) = delete;
};
void f() {
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t); // RAII:出了作用域自动 join
do_something_in_current_thread();
} // ④ 局部对象逆序销毁,先销毁 g → join 线程
RAII 精粹:把
join放进析构 +joinable()判空 + 拷贝构造拷贝赋值=delete,异常也安全。
3. 后台运行:detach()(2.1.3)
- 分离线程(daemon/守护线程) :后台独立运行,"发后即忘"(fire-and-forget)。分离后不可再 join。
- C++ 运行库保证线程退出时资源自动回收。
- 调用前必须确认
joinable()(对无执行线程 的std::thread调detach是错的)。
cpp
std::thread t(do_background_work);
t.detach();
assert(!t.joinable()); // 分离后不可 join
清单 2.4:用户编辑器/文档处理(分离线程处理任务)
cpp
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);
}
}
}
分离线程与主线程无法直接交互;数据要安全地共享(见第 3 章)。
4. 向线程函数传递参数(2.2)
传参基本机制
- 参数作为
std::thread构造函数的附加参数,拷贝到新线程内存空间。 - 即使函数参数是
引用,拷贝也会执行------ 拷贝发生在构造线程时,不是函数调用时(隐患见下)。
cpp
void f(int i, std::string const& s);
std::thread t(f, 3, "hello"); // 传 3 和字符串
悬空指针陷阱(关键)
cpp
void oops(int some_param) {
char buffer[1024]; // ① 栈上数组
sprintf(buffer, "%i", some_param);
std::thread t(f, 3, buffer); // ② 隐式 char* → 拷贝顺序不确定
t.detach(); // 线程可能在 buffer 销毁后还没完成隐式转换 → 悬空
}
COP与函数内隐式转换的顺序无法保证。 解决:显式转成
std::string再传:
cpp
std::thread t(f, 3, std::string(buffer)); // 值传递一个完整的 string
传引用:std::ref
直接传非常量引用变量会编译错(constructor 拷贝后以右值传参);用 std::ref 包住:
cpp
void update_data_for_widget(widget_id w, widget_data& data);
std::thread t(update_data_for_widget, w, std::ref(data));
成员函数作线程函数(同 std::bind 语法)
cpp
class X { public: void do_lengthy_work(int); };
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num); // 成员函数指针 + 对象指针
移动语义(只支持移动的类型)
cpp
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p)); // 必须显式 move
5. 转移所有权 std::thread(2.3)
std::thread可移动、不可拷贝 (像unique_ptr)。- 同一时刻一个实例只关联一个执行线程;移动后原对象变空(不能再 join/detach)。
cpp
std::thread t1(func1);
std::thread t2 = std::move(t1); // t1 所有权移到 t2,t1 空
t1 = std::thread(func2); // 临时线程直接赋值(移动赋值,若 t1 可 join 会先 join,否则 terminate)
6. 确定线程数量 hardware_concurrency()(2.4)
cpp
unsigned n = std::thread::hardware_concurrency(); // 硬件支持的并发线程数(核数),0 = 不可知
// 为 0 时要回退策略,比如自行选择默认值
- 并行算法常只启动并行部分所需 的线程,通常不要多于核心数。过量线程 → 上下文切换过多 → 更慢(呼应 1.2.3)。
7. 线程标识 thread::id(2.5)
- 类型
std::thread::id,可由std::thread成员get_id()或std::this_thread::get_id()获取。 - 无执行线程对象的 id 为默认构造的空 id,可判等。
std::thread::id支持比较 与哈希,可作 map 的键 / 用于检测是否为主线程。
cpp
std::thread::id main_id = std::this_thread::get_id();
std::thread::id t_id = t.get_id();
std::hash<std::thread::id> h;
if (t_id == main_id) { /* 我在主线程 */ }
8. 本章总结
| 主题 | 关键结论 |
|---|---|
| 启动 | std::thread t(f, args...),构造即启动 |
| join | 等待线程结束;只能一次;异常安全 用 RAII thread_guard |
| detach | 后台"发后即忘";先确认 joinable |
| 传参 | 默认拷贝;引用用 std::ref;const* 明转 string 防悬空;move 传所有权 |
| 所有权 | 可 move 不可 copy,RAII/所有权移交 |
| 线程数 | hardware_concurrency(),0 需回退 |
| 标识 | thread::id 可比、可哈希 |
> thread_guard 是该书第一个反"dangling"的 RAII 组件,也是后续 unique_lock 的"前奏"。
与后续章节的衔接
- 第 3 章:保护共享数据(mutex)------线程生命周期管理之后,轮到数据安全;
- 第 4 章:同步操作(CV / future)---把本章的 join 升级为"等一个计算结果/事件";
- 第 5 章:内存模型/原子 --- 真正揭示 thread 同步机制如何工作。
参考:仓库 doc/cia_ch1_concurrency_intro.md、doc/cia_ch4_sync_operations.md、doc/cia_ch5_atomic_summary.md。