Effective modern C++ 条款 40:注意不同线程句柄的析构函数的行为
来源:互联网 发布:莱昂纳德体测数据虎扑 编辑:程序博客网 时间:2024/06/03 11:36
Item 39提到,一个joinable的std::thread对象对应了一个执行线程。一个非延迟任务(见Item 38)的std::future对象与系统线程也有类似的关系。因此,std::thread和std::future对象都可以看作线程的句柄。
当然,如果你有方法可以知道触发特殊的析构行为的条件没有全部满足(根据程序逻辑),那你变可以肯定future对象不会在析构函数中阻塞。例如,只有std::async中关联的共享态才会导致future在析构中阻塞,但是还有其他地方同样会产生共享态,其中一个地方就是std::packaged_task。一个std::packaged_task对象中有一个等待异步执行的函数(或其他执行体),它的执行结果也是放入共享态。与这个共享态关联的future对象可以通过std::packaged_task的get_future函数来获得:
从这个角度来看,std::thread和std::future的析构函数的区别如此之大就变得很有趣了。如Item 39所述,析构一个joinable的std::thread对象会导致进程终止,因为两种显而易见的方案--隐式调用join和隐式调用detach--都不是一个好的选择。然而,std::future对象的析构函数中,有时会隐式调用join,有时会隐式调用detach,而有时二者都不调用。它从不会导致进程终止。
这个线程句柄的行为值得我们一探究竟。future是一个通信通道的一端,负责为被调用者传递运行结果给调用者,我们就从这个角色开始。被调用者(异步执行,延迟任务除外)将运行结果写入通信通道(通常通过std::promise对象),然后调用者通过future对象来读取这个结果。你可以认为这是一个如下所示的过程,其中虚箭头表示信息是从被调用者流向调用者的:
但是被调用者的结果存在哪呢?被调用者可能在调用者触发get函数之前就结束了,所以结果不能存储于被调用者的std::promise对象里,因为它是被调用者的局部变量,会在调用结束之后被销毁。
结果也不能存储于调用者的std::future对象中,因为它有可能被用来创建std::shared_future对象(这样一来便将被调用者运行结果的拥有权转移到std::shared_future对象),而这个对象在原始的std::future被销毁后有可能被复制多份。并且由于并不是所有的对象都是可复制的(如std::unique_ptr,见Item 20),而且被调用者的返回值的生命周期至少应该与相关的最后一个future对象一样长,那这么多个std::future对象,究竟选哪一个来存储这个结果呢?
因为调用者和被调用者都不适合存储这个返回值,所以返回值被存储于一个共享态。通常这个共享态是由一个基于堆的对象,但是它的类型、接口及实现都没有标准的定义。标准库的作者可以选择任何他们喜欢的方式来实现。
我们可以想象的到,调用者、被调用者以及共享态之间的关系如下图所示,其中虚线箭头表示信息的流向:
共享态的存在很关键,因为std::future的析构函数--这章的主题--与其相关联的共享态是息息相关的。具体而言,
- 最后一个与通过异步方式启动的std::async关联的std::future对象的析构会阻塞,直到任务结束。本质上,这个future的析构函数中隐式调用了任务的异步执行线程的join函数。
- 其它所有的std::future的析构函数仅仅销毁future对象。对于异步执行的任务,这些对象都在析构函数中调用执行线程的detach函数。对于延迟任务,意味着这个任务永远不会执行。
上述所说的对于一个future对象的异常情况只有一下所有条件都满足才会发生:
- 这个对象关联着一个由调用std::async产生的共享态
- std::async的任务启动策略是std::launch::async,要么是运行时系统自动选择或者由调用者显示指定
- 这个对象是最后一个与共享态关联的future对象。对于std::future对象总是如此,但对于std::shared_future而言,只有所有与这个共享态关联的std::shared_future都已经被销毁,那最后这个std::shared_future对象销毁之后还需要销毁它的成员变量。
你或许想知道为什么对于异步启动任务的std::async所对应的共享态会需要这种特殊处理,这么问也很合理。据我所知,标准委员会想要避免由隐式调用detach所带来的问题,但它们有不想采用直接终止进程的这种激进的做法(如析构joinable的std::thread对象--再一次,详见Item 39),所以他们最后选择了隐式调用join。这个选择并非没有争论,事实上人们曾经争论要在C++ 14中放弃这种行为。但是最终并没有这么做。
future并没有提供任何方式来查看它上是否与某个从std::async中产生的共享态关联,因此我们便无法知道它的析构函数中是否会阻塞直到异步任务执行结束。这有一些比较有意思的影响:
// this container might block in its dtor, because one or more // contained futures could refer to shared state for a non- // deferred task launched via std::async std::vector<std::future<void>> futs; // see Item 41 for info // on std::future<void> class Widget { // Widget objects might public: // block in their dtors … private: std::shared_future<double> fut; }; void doWork(std::future<int> fut); // fut might block in its dtor // (see Item 17 for info on by-value params
int calcValue(); // func to runstd::packaged_task<int()> // wrap calcValue so it pt(calcValue); // can run asynchronouslyauto ptFut = pt.get_future(); // get future for pt一旦创建,std::packaged_task的任务pt可以在另外一个线程中执行,std::packaged_task不可拷贝,所以pt必须先转成右值引用,然后传到到std::thread的构造函数,这就保证它是被moved而不是拷贝到线程的数据区间:
std::thread t(std::move(pt));
此时我们就能确定ptFut并没有关联因为调用std::async而产生的共享态,所以它的析构函数中不会阻塞。因此我们也就不必担心调用doWork时,当doWork的参数fut析构时会阻塞等待线程t结束了。
std::packaged_task::get_future返回一个std::future对象,它只能能够被move,所以为了使调用doWork能够通过编译,我们必须对ptFut使用move操作,就像我们传递pt给std::thread一样:
doWork(std::move(ptFut));
事实上可以通过这个例子看到futrure正常的析构行为,但是把所以代码放在一起会更加清晰一点:
{ // begin scope std::packaged_task<int()> // as above pt(calcValue); auto ptFut = pt.get_future(); // as above std::thread t(std::move(pt)); // as above doWork(std::move(ptFut)); // as above … // see below } // end scope最有意思的代码是"..."即在doWork调用之后,“}”之前的那段。之所以有意思是在这里面,std::thread究竟会发生什么。有三种可能的情况会发生:
- 什么都不发生。这种情况在域结束时,t将会是joinable的,这会导致程序终止。
- 在t上调用过join。这种情况ptFut已经没有必要再析构函数中阻塞了,因为join已经在代码中显示的调用了。
- 在t上调用了dtach。这种情况ptFut没有必要再析构函数中调用detach,因为它已经在代码中被显示的调用了。
重点回顾
- future的析构函数通常只销毁future的成员变量
- 最后一个与异步启动任务的std::async产生的共享态相关联的future对象会在析构函数中阻塞直到任务完成。
0 0
- Effective modern C++ 条款 40:注意不同线程句柄的析构函数的行为
- Effective Modern C++ 条款38 意识到线程句柄的析构函数的不同行为
- 《Effective C++》:条款49:了解new-handler的行为
- Effective Modern C++ 条款14 把不发出异常的函数声明为noexcept
- Effective Modern C++ 条款17 理解特殊成员函数的生成
- 《Effective Modern C++》翻译--条款4:了解如何查看推导出的类型
- Effective Modern C++ 条款16 使const成员函数成为线程安全函数
- effective C++ 条款 49:了解new-handler的行为
- Effective C++ 条款49:了解new-handler的行为
- Effective C++ 条款 49:了解new-handler的行为
- Effective C++ — 条款49:了解new-handler的行为
- 《Effective Modern C++》翻译--条款1: 理解模板类型推导
- 《Effective Modern C++》翻译--条款3: 理解decltype
- 编写可适配的函数对象(Effective stl 条款40)
- Effective Modern c++ 条款总结
- Effective Modern C++ 条款11 用deleted functions代替private undefined的做法
- Effective Modern C++ 条款18 用std::unique_ptr管理独占所有权的资源
- Effective Modern C++ 条款19 用std::shared_ptr管理共享所有权的资源
- php多进程单例模式下的 MySQL及Redis连接错误修复
- jQuery.ajax( options )
- Oracle User和Schema的区别
- 整数中1出现的次数(从1到n整数中1出现的次数)
- Gradle——故障排除
- Effective modern C++ 条款 40:注意不同线程句柄的析构函数的行为
- Spring @Resource、@Autowired、@Qualifier的注解注入及区别
- LaTeX 文件扩展名笔记
- 项目-汉语字典总结
- AsyncTask源码解析
- Python程序的执行原理(二)
- Handler
- mysql 错误代码大全
- 分布式负载测试工具Tsung安装