条款38:注意线程句柄析构函数的各种不同行为
1 线程句柄
- std::thread对象和期值对象都可以被视为系统线程的句柄。然而它们的析构函数中的行为却如此不同:
1)可join的std::thread的销毁会终止程序,因为隐式join和隐式detach——被认为是更糟糕的选择。
2)期值的销毁从不会导致程序终止,有时表现得好像它做了一个隐式join,有时又好像做了一个隐式detach,有时则两者都不是。

- 但是被调用者的结果存储在哪里呢?
1)不能存储在被调用者的std::promise中:被调用者可能在调用者调用相应的std::future的get方法之前就完成了。由于该对象是被调用者本地的,当被调用者完成时,它将被销毁。
2)不能存储在调用者的期值中:因为std::future可能用于创建std::shared_future,结果必须至少存在与最后一个引用它的期值一样长的时间,无法确定由哪个期值来包含其结果(也许结果的类型是只能移动的,不能被拷贝)。
//共享状态通常由基于堆的对象表示,但其类型、接口和实现并未由标准指定。标准库的作者可以自由地以任何方式实现共享状态。

- 共享状态的存在很重要,因为期值析构函数的行为是由与其关联的共享状态决定的:
1)经由 std::async 启动的未推迟任务,指涉到的共享状态的最后一个期值会保持阻塞,直至该任务结束。本质上,这样一个期值的析构函数是对底层异步执行任务的线程实施了一次隐式 join。
2)其他所有期值对象的析构函数只将期值对象析构就结束了。对于底层异步运行的任务,这样做类似于对线程实施了一次隐式 detach。如果这一期值是最后一个,也就意味着被推迟的任务将不会有机会运行了。 - 规则其实很简单。要处理的只是简单的“正常”行为和一个例外。
1)“正常”行为是指,期值的析构函数会销毁期值对象本身。它只是销毁期值的数据成员(以及减少共享状态中的引用计数)。
2)例外:阻塞,直到异步运行的任务完成。实际上,这相当于与运行std::async创建的任务的线程进行隐式join。 - 只有当所有下面这些条件都满足时,期值的析构函数才会表现出例外行为:
1)它指涉到由于调用 std::async 而创建的共享状态。
2)任务的启动策略是 std::launch::async:这可能是由运行时系统选择的,也可能是在调用 std::async 时指定的。
3)它是最后一个指向共享状态的期值:对于 std::future,这一点总是成立的。对于 std::shared_future,如果还有其他 std::shared_future 也指向相同的共享状态,那么它只是销毁其数据成员。 - 期值的 API 无法确定它是否涉及到由调用std::async产生的共享状态,所以对于任意的期值对象,无法知晓它是否会在其析构函数中阻塞(等待异步运行的任务完成):
// 容器可能会在其析构函数中阻塞
// 因为它包含的期值可能涉及通过std::async启动的非延迟任务的共享状态
std::vector<std::future<void>> futs; class Widget { // Widget 对象可能会在其析构函数中阻塞
public: …
private:std::shared_future<double> fut;
};
解决方法之一是使用std::packaged_task,std::packaged_task对象通过包装函数(或其他可调用对象),以便将其结果放入共享状态。可以通过std::packaged_task的get_future函数获取指向该共享状态的期值:
int calcValue(); //要运行的函数
std::packaged_task<int()> pt(calcValue); //包装 calcValue 函数,使其可以异步运行
auto fut = pt.get_future();//获取指向共享状态的期值
std::packaged_task是不可拷贝的,所以当pt传递给std::thread构造函数时,它必须被强制转换为右值:
{ // 块开始std::packaged_task<int()> pt(calcValue);auto fut = pt.get_future();std::thread t(std::move(pt));… // 见下文
} // 结束块
有趣的是t在”…”区域内会发生什么:
1)t不发生任何变化:t在作用域的末尾是可join的。这会导致项目终止。
2)对t执行了join:fut不需要在其析构函数中阻塞,因为调用fut的代码已经进行了join。
3)对t执行了detach:fut不需要在其析构函数中进行分离,因为调用fut的代码已经进行了分离。
2 要点速记
- 期值的析构函数通常只销毁对象的数据成员。
- 涉及到通过std::async启动的非延迟任务的共享状态的最后一个期值,会阻塞直到任务完成(相当于隐式join)。
