每个 std::thread
只会处于两种状态状态之一:其一为 joinable
,其二为 unjoinable
。一个 joinable
的 std::thread
对应于一个正在或可能在运行的底层线程。例如,一个对应于处于阻塞或者等待调度的底层线程的 std::thread
是 joinable
。对应于底层线程的 std::thread
已经执行完成也可以被认为是 joinable
。
而 unjoinable
的线程包括:
std::thread
。这样的 std::thread
没有执行函数,也就不会对应一个底层的执行线程。std::thread
对象已经被 move。其底层线程已经被绑定到其它 std::thread
。std::thread
已经 join
。已经 join
的对应 std::thread
的底层线程已经运行结束。std::thread
已经 detach
。已经 detach
的 std::thread
与其对应的底层线程已经没有关系了。std::thread
的 joinabilty
状态之所以重要的原因之一是:一个 joinable
状态的 std::thread
对象的析构函数的调用会导致正在运行程序停止运行。例如,我们有一个 doWork
函数,它接收一个过滤函数 filter
和一个最大值 MaxVal
作为参数。 doWork
检查并确定所有条件满足时,对 0 到 MaxVal
执行 filter
。对于这样的场景,一般会选择基于任务的方式来实现,但是由于需要使用线程的 handle 设置任务的优先级,只能使用基于线程的方法来实现(相关讨论可以参见 Item 35: Prefer task-based programming to thread-based.)。可能的实现如下:
constexpr auto tenMillion = 10000000; // see Item 15 for constexpr
bool doWork(std::function<bool(int)> filter, // returns whether
int maxVal = tenMillion) // computation was
{ // performed; see
// Item 2 for
// std::function
std::vector<int> goodVals; // values that
// satisfy filter
std::thread t([&filter, maxVal, &goodVals] // populate
{ // goodVals
for (auto i = 0; i <= maxVal; ++i)
{ if (filter(i)) goodVals.push_back(i); }
});
auto nh = t.native_handle(); // use t's native
… // handle to set
// t's priority
if (conditionsAreSatisfied()) {
t.join(); // let t finish
performComputation(goodVals);
return true; // computation was
} // performed
return false; // computation was
} // not performed
对于上面的实现,如果 conditionsAreSatisfied()
返回 true
,没有问题。如果 conditionsAreSatisfied()
返回 false
或抛出异常,std::thread
对象处于 joinable
状态,并且其析构函数将被调用,会导致执行程序停止运行。
你可能会疑惑为什么 std::thread
的析构函数会有这样的行为,那是因为其他两种选项可能更加糟糕:
join
。析构函数调用时,隐式去调用 join
等待线程结束。这听起来似乎很合理,但会导致性能异常,并且这有点反直觉,因为 conditionsAreSatisfied()
返回 false
时,也即条件不满足时,还在等待 filter
计算完成。detach
。析构函数调用时,隐式调用 detach
分离线程。doWork
可以快速返回,但可能导致 bug。因为 doWork
结束后,其内部的 goodVals
会被释放,但线程还在运行,并且访问 goodVals
,将导致程序崩溃。由于 joinable
的线程会导致严重的后果,因此标准委员会决定禁止这样的事情发生(通过让程序停止运行的方式)。这就需要程序员确保 std::thread
对象在离开其定义的作用域的所有路径上都是 unjoinable
。但是想要覆盖所有的路径并非易事,return、continue、goto、break 或者异常等都能跳出作用域。
无论何时,想在出作用域的路径上执行某个动作,常用的方法是将这个动作放入到一个局部对象的析构函数中。这种对象被成为 RAII(Resource Acquisition Is Initialization)对象,产生这个对象的类是 RAII 类。RAII 类在标准库中很常见,例如 STL 容器(每个容器的析构函数销毁容器中的内容并释放它的内存)中的智能指针(std::unique_ptr
析构函数调用它的 deleter
删除它指向的对象,std::shared_ptr
和 std::weak_ptr
的析构函数中会减少引用计数)、std::fstream
对象(析构函数关闭相应的文件)。但是 std::thread
对象没有标准的 RAII 类,这可能是标准委员会拒绝将 join
和 detach
作为默认选项,因为他们也不知道这个类应该有什么样的行为。
好在实现这样的一个类也并非难事。例如,你可以让用户指定 ThreadRAII
类在销毁时选择 join
还是 detach
:
class ThreadRAII {
public:
enum class DtorAction { join, detach }; // see Item 10 for
// enum class info
ThreadRAII(std::thread&& t, DtorAction a) // in dtor, take
: action(a), t(std::move(t)) {} // action a on t
~ThreadRAII()
{
if (t.joinable()) { // see below for
// joinability test
if (action == DtorAction::join) {
t.join();
} else {
t.detach();
}
}
}
std::thread& get() { return t; } // see below
private:
DtorAction action;
std::thread t;
};
关于上面代码的几点说明:
std::thread
的右值,因为 std::thread
不可拷贝。std:thread
为第一个参数,DtorAction
为第二个参数),但是成员变量的初始化符合成员变量的申明顺序。在这个类中两个成员变量的前后顺序没有意义,但是通常而言,一个成员的初始化依赖另一个成员。ThreadRAII
提供了 get
函数,用于访问底层的 std::thread
对象。提供 get
方法访问 std::thread
,避免了重复实现所有 std::thread
的接口。ThreadRAII
的析构函数首先检查 t
是否为 joinable
是必要的,因为对一个 unjoinable
的线程调用 join
和 detach
将产生未定义的行为。将 ThreadRAII
应用于 doWork
的例子上:
bool doWork(std::function<bool(int)> filter,
int maxVal = tenMillion)
{
std::vector<int> goodVals;
ThreadRAII t( // use RAII object
std::thread([&filter, maxVal, &goodVals]
{
for (auto i = 0; i <= maxVal; ++i)
{ if (filter(i)) goodVals.push_back(i); }
}),
ThreadRAII::DtorAction::join // RAII action
);
auto nh = t.get().native_handle();
...
if (conditionsAreSatisfied()) {
t.get().join();
performComputation(goodVals);
return true;
}
return false;
}
这个例子中,我们选择 join
作为 ThreadRAII
析构函数的动作。正如前文所述,detach
可能导致程序崩溃,join
可能导致性能异常。两害取其轻,性能异常相对可以接受。
正如 Item 17: Understand special member function generation. 所介绍的,由于 ThreadRAII
自定义了析构函数,编译器将不在自动生成移动操作,但没有理由让 ThreadRAII
对象不支持移动。因而,需要我们将移动操作标记为 default
:
class ThreadRAII {
public:
enum class DtorAction { join, detach };
ThreadRAII(std::thread&& t, DtorAction a)
: action(a), t(std::move(t)) {}
~ThreadRAII()
{
... // as before
}
ThreadRAII(ThreadRAII&&) = default; // support
ThreadRAII& operator=(ThreadRAII&&) = default; // moving
std::thread& get() { return t; } // as before
private:
DtorAction action;
std::thread t;
};
至此,本文结束。
参考: