Effective Modern C++ 条款38 意识到线程句柄的析构函数的不同行为
意识到线程句柄的析构函数的不同行为
条款37解释过一个可连接的(joinable)线程对应着一个底层的执行线程,一个非推迟任务(看条款36)的future和系统线程也有类似的关系。这样的话,可以认为std::thread对象和future对象都可以操纵系统系统。
从这个角度看,std::thread对象和future对象的析构函数表现出不同的行为是很有趣的。就如条款37提到,销毁一个可连接的std::thread对象会终止你的程序,因为另外两个选择——隐式join和隐式detach——被认为是更糟的选择。而销毁一个future,有时候会表现为隐式join,有时候会表现为隐式detach,有时候表现的行为既不是join也不是detach。它决不会导致程序终止,这种线程管理行为的方法值得我们仔细检查。
我们从观察一个future开始吧,它是一个交流管道的一端,在这个交流管道中被叫方要把结果传给主叫方。被叫方(通常异步运行)把计算的结果写进交流管道(通常借助一个std::promise对象),而主叫方使用一个future来读取结果。你可以用下图来思考,虚线箭头展示了信息被叫这流向主叫:
但被叫方的结果存储在哪里呢?在主叫方future执行get之前,被叫方可能已经执行完了,因此结果不能存储在被叫的std::promise喎? f/ware/vc/"="" target="_blank" class="keylink">vc3ryb25npsdvoapex7j2tttp86osu+hkx7g7vdc3vbxevtayv7hkwb+jrntasbu90na00nc94cr4uvo74bg7z/q72agjpc9wpg0kpha+ylu2+kosveg5+9kysrve3ltmtklu2tb3vdc3vbxeznv0dxjl1tcjrnlyzqqjqlu509dg5mv71k3s8qop0ru49jxzdhjvbmc+c3rkojpmdxr1cmu8l3n0cm9uzz621m/zv8ne3lg708patls0vajsu7j2phn0cm9uzz5zdgq6onnoyxjlzf9mdxr1cmu8l3n0cm9uzz6jqnlytmuw0bg7vdc3vb3hufu1xmv509diqlttphn0cm9uzz5zdgq6omz1dhvyztwvc3ryb25npteq0sa1vtxzdhjvbmc+c3rkojpzagfyzwrfznv0dxjlpc9zdhjvbmc+o6mjrlb41nrx7tstyry1xdxzdhjvbmc+c3rkojpmdxr1cmu8l3n0cm9uzz7p+rvz1q6686os1ek49jxzdhjvbmc+c3rkojpzagfyzwrfznv0dxjlpc9zdhjvbmc+v8ne3lvhsbu/vbg0uty24ltooapmymj0sbu90le9tcs94bn7wodqzcrhsru/ybg7v72xtlxeo6i8tna7v8nsxravwodqzaopo6y2+mthveg5+8rh1rvsqtpq0ru49mz1dhvyzdl908pl/kosy/y+zbvhtobu2qosxmfdtkostuc49mz1dhvyzdbqxmtsu7j2uqzt0lg7vdc3vbxeveg5+8tyo788l3a+dqo8cd7s8s6qsbu90le9tttp87rn1ve90le9tttp87a8srvkyrrptoa0or3hubmjrmv50ttv4rj2veg5+7tm1nrbvdxf1q7n4rxetdi3vagj1ek49rxyt7290nf2phn0cm9uzz5zagfyzwqgc3rhdgu8l3n0cm9uzz6jrdxzdhjvbmc+c2hhcmvkihn0yxrlpc9zdhjvbmc+zaizo7htz9boqtk7upa7+dpatthktc/wtcs21m/zo6y1q7hq17zdu9pq1ri2qmv8tcta4ndnoak907/aus3ktc/wo6zl+dluserxvl/itctx99xfv8ns1npdy/vdx8+yu7a1xle9t6jatmq1z9y8c3ryb25npnnoyxjlzcbzdgf0ztwvc3ryb25npqgjpc9wpg0kpha+yofpwqosztldx7/j0tsw0db3vdchorg7vdchojxzdhjvbmc+c2hhcmvkihn0yxrlpc9zdhjvbmc+1q685lxeudjptcrtzby7r6os0onp37z9zbfu2btose3p1tdfz6k1xmh3z/kjujwvcd4ncjxwpjxpbwcgywx0pq=="这里写图片描述" src="/uploadfile/collfiles/20160916/20160916093610723.png" title="\" />
shared state的存在很重要,因为future的析构函数的行为——该条款的话题——是由与它关联的shared state决定的。特别是: 这些规则听起来很复杂,但我们真正需要处理的是一个简单“正常的”行为和一个单独的例外而已。这正常的行为是:future的析构函数会销毁future对象。那意思是,它不会join任何东西,也不会detach任何东西,它也没有运行任何东西,它只是销毁 future的成员变量。(好吧。实际上,它还多做了些东西。它减少了shared state里的引用计数,这个shared state由future和被叫的std::promise共同操控。引用计数可以让库知道什么时候销毁**shared state,关于引用计数的通用知识,请看条款19.) 对于正常行为的那个例外,只有在future满足下面全部条件才会出现: 只有当这些条件都被满足时,future的析构函数才会表现出特殊的行为,而这行为是:阻塞直到异步运行的任务结束。特别说明一下,这相当于对运行着std::async创建的任务的线程执行隐式join。 这个例外对于正常的future析构函数行为来说,可以总结为“来自std::async的future在它们的析构函数里阻塞了。”对于初步近似,它是正确的,但有时候你需要的比初步近似要多,现在你已经知道了它所有的真相了。 你可能又有另一种疑问,可能是“我好奇为什么会有这么奇怪的规则?”。这是个合理的问题,关于这个我只能告诉你,标准委员会想要避免隐式detach引发的问题(看条款37),但是他们又不想用原来的策略让程序终止(针对可连接的线程,看条款37),所以他们对隐式join妥协了。这个决定不是没有争议的,他们也有讨论过要在c++14中禁止这种行为。但最后,没有改变,所以future析构函数的行为在c++11和c++14相同。 future的api没有提供方法判断future引用的shared state是否产生于std::async调用,所以给定任意的future对象,不可能知道它的析构函数是否会阻塞到异步执行任务的结束。这有一些有趣的含义: 当然,如果你有办法知道给定的future不满足触发特殊析构行为的条件(例如,通过程序逻辑),你就可以断定future不会阻塞在它的析构函数。例如,只有在std::async调用时出现的shared state才具有特殊行为的资格,但是有其他方法可以创建shared state。一个是std::packaged_task的使用,一个std::packaged_task对象包装一个可调用的对象,并且允许异步执行并获取该可调用对象产生的结果,这个结果就被放在shared state里。引用shared state的future可以借助std::packaged_task的get_future函数获取: 在这时,我们知道future对象
// 这个容器的析构函数可能会阻塞
// 因为包含的future有可能引用了借助std::async发射的推迟任务的而产生的shared state
std::vector> futs; // 关于std::future,请看条款39
class widget { // widget对象的析构函数可能会阻塞
public:
...
private:
std::shared_future fut;
};
int calcvalue(); // 需要运行的函数
std::packaged_task pt(calcvalue); // 包装calcvalue,因此它可以异步允许
auto fut = pt.get_future(); // 得到pt的future
fut没有引用由std::async调用的产生的shared state,所以它的析构函数将会表现出正常的行为。
一旦std::packaged_task对象
pt被创建,它就会被运行在线程中。(它也可以借助std::async调用,但是如果你想要用std::async运行一个任务,没有理由创建一个std::packaged_task对象,因为std::async能做std::packaged_task能做的任何事情。)
std::packaged_task不能被拷贝,所以当把
pt传递给一个std::thread构造函数时,它一定要被转换成一个右值(借助std::move——看条款23):
std::thread t(std::move(pt)); // 在t上运行pt
这个例子让我们看到了一些future正常析构行为,但如果把这些语句放在同一个块中,就更容易看出来:
{ // 块开始
std::packaged_task pt(calcvalue);
auto fut = pt.get_future();
std::thread t(std::move(pt));
... // 看下面
} // 块结束
这里最有趣的代码是“…”,它在块结束之前,
t创建之后。这里有趣的地方是在“…”中,
t会发生什么。有3个基本的可能:
t什么都没做。在这种情况下,
t在作用域结束时是可连接的(joinable),这将会导致程序终止(看条款37)。
t进行了join操作。在这种情况下,
fut就不需要在析构时阻塞了,因为代码已经join了。
t进行了detach操作。在这种情况下,
fut就不需要在析构时detach了,因为代码已经做了这件事了。
换句话说,当你shared state对应的future是由std::packaged_task产生的,通常不需要采用特殊析构策略,因为操纵运行std::packaged_task的std::thread的代码会在终止、join、detach之间做出决定。
*需要记住的2点:
喎?>
future的析构函数通常只是销毁future的成员变量。 最后一个引用shared state(它是在借助std::aysnc创建了一个非推迟任务时产生)的future会阻塞到任务完成。