为什么从`std::async` 阻塞返回未来的析构函数?

2021-12-06 00:00:00 multithreading asynchronous c++ c++11 std

当试图回答另一个 Stackoverflow 问题,我意识到这个简单的 C++11 代码段隐式地阻塞了调用线程:

std::async(std::launch::async, run_async_task)

对我来说,这似乎是规范的 C++11 异步启动任务而不关心结果的方式.相反,为了实现这一点,必须明显地创建和分离一个线程(参见 answer 提到的问题).>

所以这是我的问题:std::future 的析构函数是否有任何关于安全/正确性的原因必须阻塞?如果它只在 get 上阻塞还不够,否则,如果我对返回值或异常不感兴趣,它只是触发并忘记?

解决方案

阻止 std::async 和线程返回的期货的析构函数:这是一个有争议的话题.以下按时间顺序排列的论文列表反映了委员会成员的一些讨论:

  • N2802:请求重新考虑线程的 detach-on-destruction对象 作者:汉斯・博姆
  • N3630:异步、~future 和~线程(修订版 1) 作者:赫伯・萨特
  • N3636:~thread 应该加入 作者 Herb Sutter
  • N3637:异步和 ~future(修订版 3) 作者:赫伯・萨特、钱德勒・卡鲁斯、尼克拉斯・古斯塔夫森
  • N3679:Async() 未来的析构函数必须等待作者:汉斯・博姆
  • N3773:异步和 ~future(修订版 4) 作者:赫伯・萨特、钱德勒・卡鲁斯、尼克拉斯・古斯塔夫森
  • N3776:未来的措辞 作者 Herb Sutter
  • N3777:弃用异步的措辞 作者 Herb Sutter

尽管有很多讨论,但 C++14 没有计划改变 std::future 和 std::thread.

关于你的问题,最有趣的论文可能是 Hans Boehm 的第二篇.我引用了一些部分来回答你的问题.

<块引用>

N3679:Async() 未来的析构函数必须等待

[..] 由 async() 返回的具有 async 启动策略的 Futures 在它们的析构函数中等待相关的共享状态准备就绪.这可以防止出现关联线程继续运行的情况,并且不再有等待它完成的方法,因为关联的未来已被销毁.如果没有英勇的努力以其他方式等待完成,这样一个失控"的线程可以继续运行超过它所依赖的对象的生命周期.

[例子]

最终结果很可能是跨线程内存粉碎".如果 get()wait() 在它们 [期货] 被销毁之前被调用 [..],这个问题当然可以避免.困难 [..] 是意外的异常可能会导致该代码被绕过.因此,通常需要某种范围保护装置来确保安全.如果程序员忘记添加范围保护,攻击者可能会生成例如在适当的时候发生 bad_alloc 异常以利用疏忽,并导致堆栈被覆盖.还可以控制用于覆盖堆栈的数据,从而获得对过程的控制.这是一个非常微妙的错误,根据我们的经验,它很可能在实际代码中被忽略.

更新:Michael Wong 的旅行报告还包含一些关于 2013 年 9 月会议成果的有趣信息:

<块引用>

关于异步析构函数不应阻塞的问题,我们对此进行了大量讨论.[..] 唯一获得大量支持的立场是 [..] 给出警告,即未来的析构函数不会阻塞,除非从 async 返回,使其成为显着的例外.[..] 经过大量讨论,我们尝试携带的唯一部分是 N3776,试图澄清 ~future~shared_future 不阻塞的立场,除了可能存在异步.有一次尝试沿 C 的路线发出弃用. 弃用异步而不替换.这个议案实际上几乎是提出来的.但是 [..] 它甚至在到达手术台之前就已经死亡.

When trying to answer another Stackoverflow question, I realized that this simple C++11 snippet is implicitly blocking the calling thread:

std::async(std::launch::async, run_async_task)

To me this would have seemed the canonical C++11 way to launch a task asynchronously without caring about the result. Instead one has to apparently explicitly create and detach a thread (see answer to mentioned question) in order to achieve this.

So here's my question: Is there any reason in regards to safety/correctness that the destructor of a std::future has to be blocking? Wouldn't it be enough if it blocks on get only and otherwise, if I'm not interested in the return value or exception, it's simply fire and forget?

解决方案

Blocking destructors of futures returned by std::async and of threads: That's a controversial topic. The following list of papers in chronological order reflects some of the discussions by the members of the committee:

  • N2802: A plea to reconsider detach-on-destruction for thread objects by Hans Boehm
  • N3630: async, ~future, and ~thread (Revision 1) by Herb Sutter
  • N3636: ~thread Should Join by Herb Sutter
  • N3637: async and ~future (Revision 3) by Herb Sutter, Chandler Carruth, Niklas Gustafsson
  • N3679: Async() future destructors must wait by Hans Boehm
  • N3773: async and ~future (Revision 4) by Herb Sutter, Chandler Carruth, Niklas Gustafsson
  • N3776: Wording for ~future by Herb Sutter
  • N3777: Wording for deprecating async by Herb Sutter

Although there was a lot of discussion, there are no changes planned for C++14 regarding the blocking behaviour of the destructors of std::future and std::thread.

Regarding your question, the most interesting paper is probably the second by Hans Boehm. I quote some parts to answer your question.

N3679: Async() future destructors must wait

[..] Futures returned by async() with async launch policy wait in their destructor for the associated shared state to become ready. This prevents a situation in which the associated thread continues to run, and there is no longer a means to wait for it to complete because the associated future has been destroyed. Without heroic efforts to otherwise wait for completion, such a "run-away" thread can continue to run past the lifetime of the objects on which it depends.

[Example]

The end result is likely to be a cross-thread "memory smash". This problem is of course avoided if get() or wait() is called [..] before they [the futures] are destroyed. The difficulty [..] is that an unexpected exception may cause that code to be bypassed. Thus some sort of scope guard is usually needed to ensure safety. If the programmer forgets to add the scope guard, it appears likely that an attacker could generate e.g. a bad_alloc exception at an opportune point to take advantage of the oversight, and cause a stack to be overwritten. It may be possible to also control the data used to overwrite the stack, and thus gain control over the process. This is a sufficiently subtle error that, in our experience, it is likely to be overlooked in real code.

Update: Michael Wong's Trip Report also contains some interesting information regarding the outcomes of the meeting in September 2013:

The View from the C++ Standard meeting September 2013 Part 2 of 2.

On the issue that async destructors should not block we devoted a great deal of discussion on it. [..] The only position that received considerable support was [..] giving advisory that future destructors will not block, unless returned from async, making it the notable exception. [..] After significant discussion, the only part that we tried to carry was N3776, an attempt to clarify the position that ~future and ~shared_future don’t block except possibly in the presence of async. There was an attempt to issue a deprecation along the lines of C. Deprecate async without replacement. This motion was actually almost put forward. But [..] it died even before it reached the operating table.

相关文章