Why is the destructor of a future returned from `std::async` blocking?
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?
Solution 1:
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()
withasync
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()
orwait()
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.