Effective Modern C++ 条款38 意识到线程句柄的析构函数的不同行为

本文探讨了std::future析构函数的不同行为,特别是在与std::async关联时如何阻塞直至任务完成,以及如何通过std::packaged_task管理这些行为。

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

条款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会阻塞到任务完成。
Coming to grips with C++11 and C++14 is more than a matter of familiarizing yourself with the features they introduce (e.g., auto type declarations, move semantics, lambda expressions, and concurrency support). The challenge is learning to use those features effectively—so that your software is correct, efficient, maintainable, and portable. That’s where this practical book comes in. It describes how to write truly great software using C++11 and C++14—i.e. using modern C++. Topics include: The pros and cons of braced initialization, noexcept specifications, perfect forwarding, and smart pointer make functions The relationships among std::move, std::forward, rvalue references, and universal references Techniques for writing clear, correct, effective lambda expressions How std::atomic differs from volatile, how each should be used, and how they relate to C++'s concurrency API How best practices in "old" C++ programming (i.e., C++98) require revision for software development in modern C++ Effective Modern C++ follows the proven guideline-based, example-driven format of Scott Meyers' earlier books, but covers entirely new material. "After I learned the C++ basics, I then learned how to use C++ in production code from Meyer's series of Effective C++ books. Effective Modern C++ is the most important how-to book for advice on key guidelines, styles, and idioms to use modern C++ effectively and well. Don't own it yet? Buy this one. Now". -- Herb Sutter, Chair of ISO C++ Standards Committee and C++ Software Architect at Microsoft
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值