- 每个
std::thread
对象随时处于两种状态之一:joinable 或 unjoinable。处于 joinable 状态意味着std::thread
对象必须对其内部的线程资源负责,该线程的状态可以是等待执行、正在执行或已经完成执行。而 unjoinbale 状态正相反,具体包括几种情况:- 默认构造的
std::thread
。它们没有要执行的函数,因此也不会真正持有一个线程。 - 已经被用于移动构造其它对象的
std::thread
。移动操作将其内部持有线程的控制权交给了其它对象,因此它不再负责管理该线程。 - 已经被
join
的std::thread
。进行join
后,std::thread
对象不再对应其内部已经完成执行的线程。 - 已经被
detach
的std::thread
。detach
会切断std::thread
对象与其内部线程之间的关联。
- 默认构造的
-
必须保证所有
std::thread
在最终处于 unjoinable 状态最直接的原因是:如果一个 joinable 的std::thread
的析构函数被调用,那么程序执行会直接被终结。这样的行为是因为std::thread
能选择的另外两种行为都很糟糕:- 隐式执行
join
。这种情况下,std::thread
的析构函数必须等待线程的函数执行完成,这从逻辑上听起来较为合理,但会导致难以追踪的性能异常。 - 隐式执行
detach
。这种情况下,std::thread
直接切断自身与线程的连接,线程将继续执行下去。这可能引发更难以debug的问题,例如该线程执行的 lambda 函数从创建std::thread
的f
函数中按引用捕获了一个局部变量。一段时间后f
已经执行完成并返回,其栈帧被弹出,而该线程被与std::thread
对象切断后继续执行。假设将来某个函数g
被调用时占用了f
原来的空间,而线程在执行中改变了其引用变量的值,那么在g
看来,其栈帧内的内存竟然自行发生了变化!
- 隐式执行
-
因此,C++标准禁止了销毁 joinable 线程的行为。
-
于是正如本节标题所述,我们应保证
std::thread
对象在所有运行路径上最终是 unjoinable 的。这与所有的资源管理问题是类似的,我们不应该手动检查所有的执行路径,而应该使用 RAII,通过任何对象在超出生存范围(scope,体现为大括号)时必定会被调用析构函数的原理做它们的管理。C++标准库中没有对std::thread
的 RAII 管理类,但我们可以自己动手写一个:
class ThreadRAII
{
public:
enum class DtorAction { join, detach };
// 仅允许 std::thread 参数的移动构造,不允许拷贝构造
// 成员初始化中,由于线程初始化后可能立刻开始执行,一般把它放在最后
ThreadRAII(std::thread&& t, DtorAction a) : action(a), t(std::move(t)) {}
// 声明析构函数导致 ThreadRAII 的移动构造和移动赋值函数不会被自动生成
// 实际上默认生成的版本(member-wise 移动 t 和 action)没有问题,因此把它们加回来
ThreadRAII(ThreadRAII&&) = default;
ThreadRAII& operator=(ThreadRAII&&) = default;
// get 接口函数
std::thread& get() { return t; }
~ThreadRAII() {
// 必须先检查,对 unjoinable 的线程进行 join 或 detach 会导致 undefined behavior
if (t.joinable()) {
if (action == DtorAction::join) {
t.join();
} else {
t.detach();
}
}
}
private:
std::thread t;
DtorAction action;
};
int doAsyncWork() { ... }
ThreadRAII t(std::thread(doAsyncWork), ThreadRAII::DtorAction::join);
// 通过 get 函数访问内部的线程(进一步访问 native_handle 或进行其它操作)
auto nh = t.get().native_handle();
// 假设 t 此时应该被 join
t.get().join();
// 即使不写上句在离开右括号时 t 也会被按构造函数中声明的 DtorAction::join 或 detach 操作
总结
- 保证 std::thread 在所有运行路径上最终是 unjoinable 的。
- 销毁时 join 的行为可能导致难以debug的性能异常。
- 销毁时 detach 的行为可能导致难以debug的 undefined behavior。
- 将
std::thread
对象放在成员(初始化)列表的最后。