Why Is the Destructor of a Future Returned from 'Std::Async' Blocking

Why is the destructor of a future returned from `std::async` blocking?

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.

Is it possible to create std future with blocking destructor without calling std async?

You could make a light wrapper for the std::future.

#include <future>

template <typename T>
class MyBlockingFuture {
std::future<T> future;

public:
MyBlockingFuture(std::future<T>&& f) : future(std::move(f)) {}
~MyBlockingFuture() {
if (future.valid()) {
future.wait();
}
}
};

Then add whatever get or wait methods you need.


Making std::future behave this way is not possible without modifying it.

Also note that there is no guarantee that the shared state held by a promise from std::async will block when destructed. The note only says that it may do so.

std::future returned from std::async hangs while going out of scope

Taking from cppreference sample, only "the start", "f2 finished" and "the end" will get printed from this code (because f1 doesn't "hang"):

#include <future>
#include <thread>
#include <iostream>

int main() {
using namespace std::literals;

{
std::packaged_task<int()> task([]() {
std::this_thread::sleep_for(5s);
std::cout << "f1 finished" << std::endl;
return 42;
});
std::future<int> f1 = task.get_future();
std::thread(std::move(task)).detach();

std::future<int> f2 = std::async(std::launch::async, []() {
std::this_thread::sleep_for(3s);
std::cout << "f2 finished" << std::endl;
return 42;
});

f1.wait_for(1s);
f2.wait_for(1s);
std::cout << "the start" << std::endl;
}

// std::this_thread::sleep_for(7s);
std::cout << "the end" << std::endl;
}

For good discussion see: http://scottmeyers.blogspot.com.br/2013/03/stdfutures-from-stdasync-arent-special.html.

C++ standard library gives no support for thread kill operations.

Take care with threads you detach. Detachment per se is not "extremely bad", it may be useful in user terminable daemons for example, or if you have some other idea of orchestration and teardown. Otherwise, detach would have no point being provided by the standard library.

Task executed with std::async is blocking like if future was used

Yes, std::future returned by async has the special property of waiting for the task to be completed in the destructor.

This is because loose threads are bad news, and the only token you have to wait for that thread is in the destructor of the future.

To fix this, store the resulting futures until either you need the result to be done, or in extreme cases the end of the program.

Writing your own thread pool system is also a good idea; I find C++ threading primitives to be sufficient to write a threading system, but use in the raw is not something I'd encourage outside of tiny programs.

Why std::future is different returned from std::packaged_task and std::async?

std::async has definite knowledge of how and where the task it is given is executed. That is its job: to execute the task. To do that, it has to actually put it somewhere. That somewhere could be a thread pool, a newly created thread, or in a place to be executed by whomever destroys the future.

Because async knows how the function will be executed, it has 100% of the information it needs to build a mechanism that can communicate when that potentially asynchronous execution has concluded, as well as to ensure that if you destroy the future, then whatever mechanism that's going to execute that function will eventually get around to actually executing it. After all, it knows what that mechanism is.

But packaged_task doesn't. All packaged_task does is store a callable object which can be called with the given arguments, create a promise with the type of the function's return value, and provide a means to both get a future and to execute the function that generates the value.

When and where the task actually gets executed is none of packaged_task's business. Without that knowledge, the synchronization needed to make future's destructor synchronize with the task simply can't be built.

Let's say you want to execute the task on a freshly-created thread. OK, so to synchronize its execution with the future's destruction, you'd need a mutex which the destructor will block on until the task thread finishes.

But what if you want to execute the task in the same thread as the caller of the future's destructor? Well, then you can't use a mutex to synchronize that since it all on the same thread. Instead, you need to make the destructor invoke the task. That's a completely different mechanism, and it is contingent on how you plan to execute.

Because packaged_task doesn't know how you intend to execute it, it cannot do any of that.

Note that this is not unique to packaged_task. All futures created from a user-created promise object will not have the special property of async's futures.

So the question really ought to be why async works this way, not why everyone else doesn't.

If you want to know that, it's because of two competing needs: async needed to be a high-level, brain-dead simple way to get asynchronous execution (for which sychronization-on-destruction makes sense), and nobody wanted to create a new future type that was identical to the existing one save for the behavior of its destructor. So they decided to overload how future works, complicating its implementation and usage.

Does std::future wait on destruction

From: 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.

Also check: N3679: Async() future destructors must wait

The basic issue

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.

As an example, consider the following pair of functions:

void f() {
vector<int> v;
...
do_parallel_foo(v);
...
}

void do_parallel_foo(vector<int>& v) {
auto fut = no_join_async([&] {... foo(v); return ...; });
a: ...
fut.get();
...
}

If no_join_async() returns a future whose destructor does not wait for
async completion, everything may work well until the code at a throws
an exception. At that point nothing waits for the async to complete,
and it may continue to run past the exit from both do_parallel_foo()
and f(), causing the async task to access and overwite memory
previously allocated to v way past it's lifetime.

The end result is likely to be a cross-thread "memory smash" similar
to that described in N2802 under similar conditions.

This problem is of course avoided if get() or wait() is called on
no_join_async()-generated futures before they are destroyed. The
difficulty, as in N2802, 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.

non-blocking call of std::async: how is this version dangerous?

It's undefined behaviour to end the lifetime of a non-trivial object without calling it's destructor, which happens as soon as there is a second call_async invocation.

"heap-allocation related overhead" is a misnomer if the only alternative is undefined behaviour. The future returned by async has to live somewhere.

The updated code has defined behaviour: it waits for the previous invocation to be done before launching the next one.



Related Topics



Leave a reply



Submit