The flexibility that default policy for std::async
permits both async and sync task execution leads to uncertainty when accessing thread_locals
, implies that the task may never execute, and affects program logic for timeout-based wait
calls.
When requesting a function f
to be run in accord with a std::async
, there are two standard launch policies, each represented by an enumerator in the std::launch
scoped enum:
- The
std::launch::async
launch policy:f
must be run asynchronously on a different thread. - The
std::launch::deferred
launch policy:f
may run only whenget
orwait
is called on the future returned bystd::async
.
In the 2nd policy, things go like this: f
’s execution is deferred until a caller invokes get
or wait
, upon which time f
will execute synchronously and block the caller until f
finishes running. If neither get
nor wait
is called, f
will never run.
The default launch policy, however, uses an OR-ed version of the above two policies:
|
|
This default policy permits f
to be run either asynchronously or synchronously, depending on the decision of thread-management components of the Standard Library about the best step to do to avoid oversubscription and load balancing. This flexibility, however, also imtroduces some limitations:
-
It’s not possible to predict whether
f
will run concurrently witht
, sincef
might be scheduled to run deferred. -
It’s not possible to predict whether
f
runs on a thread different from the thread invokingget
orwait
onfut
-
It may not be possible to predict whether
f
runs at all, since it may not be possible to guarantee thatget
orwait
will be called onfut
along every path through the program -
It affects
wait
-based loops using timeouts, since callingwait_for
orwait_until
on a task that’s deferred yields the valuestd::future_status_deferred
, leading to following code run forever in some special cases:1 2 3 4 5 6 7 8 9 10 11 12 13
using namespace std::literals; void f() { std::this_thread::sleep_for(1s); } auto fut = std::async(f); // run f in default launch policy while (fut.wait_for(100ms) != // f never yield read std::future_status::ready) // when deferred { ... }
Solution
To deal with these limitations, if we use both the default launch policy as well as the timeout-based wait
calls, we need to check for deferred case:
|
|
In summary, using std::async
with the default launch policy for a task is fine as long as following conditions are fulfilled:
- The task need not run concurrently with the thread calling
get
orwait
- It doesn’t matter which thread’s thread_local variables are read or written
- Either there’s a guarantee that
get
orwait
will be called on the future returned bystd::async
or it’s acceptable that the task may never execute - Code using
wait_for
orwait_until
takes the possibility of deferred status into account like above example.
If any of the conditions above fails to hold, schedule the task for truly asynchronous execution:
|
|
A nice wrapper for this purpose goes like this:
|
|
|
|