Item 37 介绍了 std::thread 对应一个底层的系统执行线程,一个非 deferred 任务的 future(这里包括 std::future 和 std::shared_future,下同)也对应一个底层的系统执行线程。一个 joinable 的 std::thread 对象析构时会导致程序终止,因为隐式的 join() 和隐式的 detach() 可能会更加糟糕。但是,future 的析构函数可能是隐式地执行 detach(),也可能是隐式地执行 join(),或者二者皆不是。本 Item 将和大家探讨下这个问题。
直观地观察,被调用者(callee)和调用者(caller)之间有一个通信通道(channel),callee 异步执行完成后,将结果写入(通常通过 std::promise 对象)这个通道,caller 通过 future 读取结果。类似下面这个模型:

但是,这个模型有点问题:callee 的结果存储在哪里?首先,不能存储在 callee 的 std::promise 对象中,因为 caller 在调用 future 的 get 方法之前,callee 可能已经结束了,callee 的局部变量 std::promise 已经销毁了。再者,callee 的结果也不能存储在 caller 的 std::future 中,因为 std::future 可以用来创建 std::shared_future ,那么这个结果就需要被拷贝多次,不是所有结果的类型都是可以拷贝的。其实 callee 的结果是被存储在独立于 caller 和 callee 之外的特殊位置,被成为共享状态(shared state)的位置。模型如下:

由于这个共享状态的存在, future 的析构函数的行为则与这个共享状态关联的 future 决定:
std::async 发起的非 deferred 的任务的返回的 future 对象,并且它是最后一个引用共享状态的,那么它的析构会一直阻塞到任务完成,也就是隐式执行 join()。future 只是简单的销毁。对于异步任务,类似隐式执行 detach(),对于 deferred 策略的任务则不再运行。反过来看,future 会隐式执行 join() 需要满足下面 3 个条件:
future 是由 std::async 创建产生,并且引用共享状态。std::async 指定的任务策略是 std::launch::async。future 是最后一个引用共享状态的对象。future 的 API 没有提供它是否由 std::async 产生并指向共享状态,因此,对于任意的 future,我们无法知道它的析构函数会不会阻塞到任务完成。
// this container might block in its dtor, because one or more
// contained futures could refer to a shared state for a non-
// deferred task launched via std::async
std::vector<std::future<void>> futs; // see Item 39 for info
// on std::future
class Widget { // Widget objects might
public: // block in their dtors
...
private:
std::shared_future<double> fut;
};
如果你知道给定的 future 不满足上述 3 个条件,那么其析构函数就不会阻塞住。例如,future 来自于 std::packaged_task:
int calcValue(); // func to run
std::packaged_task<int()> // wrap calcValue so it
pt(calcValue); // can run asynchronously
auto fut = pt.get_future(); // get future for pt
一般地, std::packaged_task pt 需要运行在一个 std::thread 上, std::packaged_task 的 future 是的析构是结束程序,还是执行 join,异或执行 detach,交给了对应的 std::thread 接下来的行为了:
{ // begin block
std::packaged_task<int()>
pt(calcValue);
auto fut = pt.get_future();
std::thread t(std::move(pt)); // std::packaged_task 不可拷贝
... // see below
} // end block
“…” 就是接下来操作 t 的代码,可能如下:
t 什么也没做(没有 join 也没有 detach)。在 block 结束时, t 是 joinable 的,这会导致程序终止。t 执行 join 。这个时候, fut 的析构函数没有必要再执行 join。t 执行 detach 。这个时候, fut 的析构函数没有必要再执行 detach。下面给一个测例作为验证:
#include
#include
#include
#include
using namespace std::chrono_literals;
void func() {
std::this_thread::sleep_for(3s);
}
int main() {
{
std::packaged_task<void()> pt(func);
auto fut = pt.get_future();
std::thread t(std::move(pt));
// t.join();
// t.detach();
}
std::cout << "hello!" << std::endl;
return 0;
}
执行结果如下:
terminate called without an active exception
Aborted (core dumped)
将 join 或者 detach 部分代码打开,则不会产生上述问题。
至此,本文结束。
参考: