-
std::thread
和非延迟的std::future
内部都对应着一个实际的线程,可以认为这些对象是我们对系统线程的 handle。 -
从这个角度来看,
std::thread
和 future 在析构函数中有不同的行为是比较奇怪的:如 Item 37 所述,销毁一个 joinable 的std::thread
会导致程序终止;然而销毁 future 有时会表现地如隐式执行了 join,有时如隐式执行了 detach,有时二者皆非,但无论如何都不会导致程序终止。本节中,我们就对 future 的行为进行更仔细的观察。 -
首先,future 位于调用者(caller)和被调用者(callee)通信信道中调用者一侧。被调用者将其计算结果(一般通过一个
std::promise
对象)写入信道,而调用者通过 future 的get()
函数读取该结果,示意图如下:
-
这里就有一个问题:被调用者的结果是存储在哪里的呢?
- 不能被存储在被调用者创建的
std::promise
对象中。因为被调用者可能在调用者通过get()
获取结果前就已经结束执行,其创建的所有对象已被销毁。 - 也不能存储在调用者的 future 中,因为
std::future
可能被用来创建std::shared_future
并将被调用者的结果转交给后者。std::shared_future
可以被拷贝,然而不是所有结果类型都可以(例如 move-only 的类型),所以它只能被放在其中一个 future 对象中。然而多个 future 对象怎么知道自己是否是应该持有结果的那个呢?
- 不能被存储在被调用者创建的
-
结果实际是被存储在一个二者之外的地方,称为 shared state,通常是一个在堆上分配的对象,但是具体实现C++标准没有规定。调用者,被调用者和 shared state 间的数据流向如下图所示:
-
future 对象的析构函数的行为就由与其关联的 shared state 决定:
- 引用同一个 通过
std::async
创建 的 非延迟 的 shared state 的 最后一个 future 对象的析构函数,会 阻塞 直至执行完成,相当于对线程进行了隐式的 join 。 - 其它所有 future 对象的析构函数只是销毁自身,类似于对线程隐式执行了 detach。对于最后一个持有某延迟的任务的 future 对象,这也意味着延迟的任务永远不会被执行。
- 引用同一个 通过
-
可以看出,相比
std::thread
,future 采取了更“温和”的措施:隐式的 join,而非终止程序。标准委员会对此也有争议,不过这就是C++11/14最终的决定。 -
如果你能从程序逻辑上判断一个 future 对象满足以上三个条件中的任意一个,那么其析构函数就不会阻塞程序;否则就有可能阻塞(而且无法通过其它方式判断)。
-
如果需要确保 future 不会阻塞,那么可以通过打破 “通过
std::async
创建” 这个条件实现,具体来说可以使用std::packaged_task
。该对象可以认为比std::async
低一层级,其作用是将一个函数包装成结果会被放入一个 shared state 的样子,以供异步执行。它的 future 对象可以通过get_future()
获得。创建后,它可以被放在一个std::thread
上运行:
{ // begin block
std::packaged_task<int()>
pt(calcValue);
auto fut = pt.get_future();
std::thread t(std::move(pt)); // std::packaged_task can't be copied
... // see below
} // end block
如此,程序的行为规则将遵从 std::thread
的规则,在 “…” 的部分中:如果执行了 join 或 detach,则 future 在析构函数中无需再次如此做;否则线程 t 将会在作用域结束时仍然 joinable,导致程序终止。future 的析构函数不会再有阻塞的可能。
总结
- future 的析构函数大部分情况下只是销毁其数据成员。
- 引用同一个通过
std::async
创建的非延迟的 shared state 的最后一个future 对象的析构函数会阻塞直至任务执行完成。