std::thread
的joinable之所以重要的原因之一是:如果可以被join的线程对象的析构函数被调用,则程序的执行就终止了。
假设我们有一个函数 doWork
,它接收一个筛选器函数 filter
和一个最大值 maxVal
作为形参。 doWork
会校验它做计算的条件全部成立,而后会针对筛选器选出的0到 maxVal
之间的值实施计算。如果筛选是费事的,而条件检验也是费事的,那么并发地执行就是合理的:
constexpr auto tenMillion = 10'000'000;
bool doWork(std::function<bool(int)>filter,
int maxVal = tenMillion)
{
std::vector<int> goodVals;
std::thread t([&filter, maxVal, &goodVals]
{
for (auto i = 0; i <= maxVal; ++i)
{ if (filter(i)) goodVals.push_back(i); }
});
auto nh = t.native_handle();
...
if (conditionsAreSatisfied()) {
t.join();
performComputation(goodVals);
return true;
}
return false;
}
如果 conditionsAreSatisfied()
返回 false
或抛出异常,那么 doWork
的末尾调用 std::thread
类型对象 t
的析构函数时,它是joinable的,从而导致程序执行终止。
如果你使用了 std::thread
对象,就得确保从它定义的作用域出去的任何路径,使它成为非joinable的。最好的方法是使用RAII技法。C++20提供了 std::jthread
,在线程对象被销毁时自动 join
线程。我们也可以自己实现:
class ThreadRAII {
public:
enum class DtorAction { join, detach };
ThreadRAII(std::thread&& t, DtorAction a)
: action(a), t(std::move(t)) {}
~ThreadRAII()
{
if (t.joinable()) {
if (action == DtorAction::join) {
t.join();
} else {
t.detach();
}
}
}
std::thread& get() { return t; }
private:
DtorAction action;
std::thread t;
};
虽然在这里不会产生什么影响,但是建议将 std::thread
对象在成员列表的最后声明。
在 doWork
中使用 ThreadRAII
:
bool doWork(std::function<bool(int)>filter,
int maxVal = tenMillion)
{
std::vector<int> goodVals;
ThreadRAII t(
std::thread([&filter, maxVal, &goodVals]
{
for (auto i = 0; i <= maxVal; ++i)
{ if (filter(i)) goodVals.push_back(i); }
}),
ThreadRAII::DtorAction::join
);
auto nh = t.get().native_handle();
...
if (conditionsAreSatisfied()) {
t.get().join();
performComputation(goodVals);
return true;
}
return false;
}
条款39会展示,使用 ThreadRAII
在 std::thread
析构中实施 join
不仅会导致性能异常,还会导致程序无响应。解决方案是和异步执行的lambda表达式通信,这里的实现太过复杂,不做讨论。
显式声明移动操作对于 ThreadRAII
是合适且必要的:
class ThreadRAII {
public:
enum class DtorAction { join, detach };
ThreadRAII(std::thread&& t, DtorAction a)
: action(a), t(std::move(t)) {}
~ThreadRAII()
{
if (t.joinable()) {
if (action == DtorAction::join) {
t.join();
} else {
t.detach();
}
}
}
ThreadRAII(ThreadRAII&&) = default;
ThreadRAII& operator=(ThreadRAII&&) = default;
std::thread& get() { return t; }
private:
DtorAction action;
std::thread t;
};