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

然而,结果也不能存储在主叫方的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<std::future<void>> futs;           // 关于std::future<void>,请看条款39

class Widget {                // Widget对象的析构函数可能会阻塞
public:
    ...
private:
    std::shared_future<double> 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<int()> pt(calcValue);   // 包装calcValue,因此它可以异步允许

auto fut = pt.get_future();     // 得到pt的future

在这时,我们知道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<int()> 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_taskstd::thread的代码会在终止、joindetach之间做出决定。
*需要记住的2点:

  • future的析构函数通常只是销毁future的成员变量。
  • 最后一个引用shared state(它是在借助std::aysnc创建了一个非推迟任务时产生)的future会阻塞到任务完成。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值