设为首页 加入收藏

TOP

Effective Modern C++ 条款38 意识到线程句柄的析构函数的不同行为(一)
2016-09-16 23:03:14 】 浏览:649
Tags:Effective Modern 条款 意识 线程 函数 不同 行为

意识到线程句柄的析构函数的不同行为

条款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里。那个对象,会是被叫方的局部变量,在被叫执行结束后会被销毁。

然而,结果也不能存储在主叫方的future中,因为(还有其他原因)一个std::future对象可能被用来创建一个std::shared_future(因此把被叫方结果的所有权从std::future转移到std::shared_future),而在最原始的std::future销毁之后,这个std::shared_future可能会被拷贝很多次。倘若被叫方的结果类型是不可被拷贝的(即只可移动类型),而那结果是只要有一个future引用它,它就会存在,那么,多个future中哪一个含有被叫方的结果呢?

因为被叫方对象和主叫方对象都不适合存储结构,所以这个结果存在两者之外的地方。这个地方叫做shared stateshared state通常表现为一个基于堆实现的对象,但标准没有指定它的类型、接口和实现,所以标准库的作者可以用他们喜欢的方法来实现shared state

如下,我们可以把主叫、被叫、shared state之间的关系视图化,虚线箭头再次表现信息的流向:

这里写图片描述

shared state的存在很重要,因为future的析构函数的行为——该条款的话题——是由与它关联的shared state决定的。特别是:

最后一个引用shared state(它借助std::aysnc创建了一个非推迟任务时产生)的future的析构函数会阻塞直到任务完成。本质上,这种future的析构函数对底层异步执行任务的线程进行隐式的 join其他的future对象的析构函数只是简单地销毁future对象。对于底层异步运行的任务,与对线程进行 detach操作相似。对于最后一个future是推迟的任务的情况,这意味着推迟的任务将不会运行。

这些规则听起来很复杂,但我们真正需要处理的是一个简单“正常的”行为和一个单独的例外而已。这正常的行为是:future的析构函数会销毁future对象。那意思是,它不会join任何东西,也不会detach任何东西,它也没有运行任何东西,它只是销毁 future的成员变量。(好吧。实际上,它还多做了些东西。它减少了shared state里的引用计数,这个shared state由future和被叫的std::promise共同操控。引用计数可以让库知道什么时候销毁**shared state,关于引用计数的通用知识,请看条款19.)

对于正常行为的那个例外,只有在future满足下面全部条件才会出现:

future引用的shared state是在调用了std::async时被创建任务的发射策略是std::launch::async(看条款36),要么是运行时系统选择的,要么是调用 std::async时指定的。 这个future是最后一个引用shared state的future。对于 std::future,它总是最后一个,而对于 std::shared_future,当它们被销毁的时候,如果它们不是最后一个引用 shared state的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有可能引用了借助std::async发射的推迟任务的而产生的shared state
std::vector
  
   > futs;           // 关于std::future
   
    ,请看条款39 class Widget { // Widget对象的析构函数可能会阻塞 public: ... private: std::shared_future
    
      fut; };
    
   
  

当然,如果你有办法知道给定的future不满足触发特殊析构行为的条件(例如,通过程序逻辑),你就可以断定future不会阻塞在它的析构函数。例如,只有在std::async调用时出现的shared state才具有特殊行为的资格,但是有其他方法可以创建shared state。一个是std::packaged_task的使用,一个std::packaged_task对象包装一个可调用的对象,并且允许异步执行并获取该可调用对象产生的结果,这个结果就被放在shared state里。引用shared state的future可以借助std::packaged_taskget_future函数获取:

int calcValue();         // 需要运行的函数

std::packaged_task
  
    pt(calcValue);   // 包装calcValue,因此它可以异步允许

auto fut = pt.g
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇C++基础复习心得2 下一篇Java客户端上传图片(文件)到c++..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目