During a code review, I came across a piece of code that basically boils down to this:
#include <iostream>
#include <future>
#include <thread>
int main( int, char ** )
{
std::atomic<int> x( 0 );
std::future<void> task;
for( std::size_t i = 0u; i < 5u; ++i )
{
task = std::async( std::launch::async, [&x, i](){
std::this_thread::sleep_for( std::chrono::seconds( 2u * ( 5u - i ) ) );
++x;
} );
}
task.get();
std::cout << x << std::endl;
return 0;
}
I was not quite sure whether
I could not answer that question from reading documentation on the internet, so I thought I would write the snippet above to find out what our compiler actually does.
Now, I found out that the answer of what gcc-5 does is indecisive and that made me even more curious: One would assume that the assignment is either blocking or non-blocking.
If it is blocking, the time taken by the program should basically be the sum of the time the individual tasks take to execute. The first one takes 10 seconds, the second 8, the third 6, the fourth 4 and the last 2 seconds. So in total it should take 10+8+6+4+2 = 30 seconds.
If it is non-blocking, it should take as long as the last task, i.e. 2 seconds.
Here is what happens: It takes 18 seconds (measured using time ./a.out or a good old clock). By playing around a bit with the code I found out that the code behaves as if the assignment would be alternatingly blocking and non-blocking.
But this can't be true, right? std::async
probably falls back to std::deferred
half of the time? My debugger says that it spawns two threads, blocks until both threads exit, then spawns two more threads and so on.
What does the standard say? What should happen? What happens inside gcc-5?
The five stages of change are precontemplation, contemplation, preparation, action, and maintenance. Precontemplation is the stage at which there is no intention to change behavior in the foreseeable future. Many individuals in this stage are unaware or underaware of their problems.
The Stages of Change Contemplation (Acknowledging that there is a problem but not yet ready, sure of wanting, or lacks confidence to make a change) Preparation/Determination (Getting ready to change) Action/Willpower (Changing behavior)
Stage 1: Precontemplation The earliest stage of change is known as precontemplation. 1 During the precontemplation stage, people are not considering a change. People in this stage are often described as "in denial," because they claim that their behavior is not a problem.
Definition: The Relapse Stage is the sixth stage of change in the Transtheoretical Model and represents the time in a person's treatment where they have slipped back into old habits and returned to use. Relapse is said to happen when people lose sight of their recovery.
In general, the assignments of task
via operator=(&&)
do not have to be blocking (see below), but since you created the std::future
using std::async
, these assignments become blocking (thanks to @T.C.):
[future.async]
If the implementation chooses the launch::async policy,
[...]
the associated thread completion synchronizes with ([intro.multithread]) the return from the first function that successfully detects the ready status of the shared state or with the return from the last function that releases the shared state, whichever happens first.
What happens in your case is that std::async
starts the "thread" for your lambda before the assignment — See below for a detailed explanation on how you get an execution time of 18 seconds.
This is what (probably) happens in your code (e
stands for an epsilon):
t = 0
, first std::async
call with i = 0
, starting a new thread;t = 0 + e
, second std::async
call with i = 1
starting a new thread, then move. The move will release the current shared state of task
, blocking for about 10 seconds (but the second std::async
with i = 1
is already executing);t = 10
, third std::async
call with i = 2
starting a new thread, then move. The current shared state of task
was the call with i = 1
which is already ready so nothing blocks;t = 10 + e
, fourth std::async
call with i = 3
starting a new thread, then move. The move is blocking because the previous std::async
with i = 2
is not ready but the thread for i = 3
as already started;t = 16
, fifth std::async
call with i = 4
starting a new thread, then move. The current shared state of task
(i = 3
) is already ready, so non-blocking;t = 16 + e
, out of the loops, call to .get()
wait for the *shared state` to be ready;t = 18
, shared state becomes ready so the whole stuff ends.std::future::operator=
:Here is the standard quote for operator=
on std::future
:
future& operator=(future&& rhs) noexcept;
Effects:
- (10.1) — releases any shared state (30.6.4).
- ...
And here is what "Releases any shared state" mean (emphasis is mine):
When an asynchronous return object or an asynchronous provider is said to release its shared state, it means:
(5.1) — [...]
(5.2) — [...]
(5.3) — these actions will not block for the shared state to become ready, except that it may block if all of the following are true: the shared state was created by a call to std::async, the shared state is not yet ready, and this was the last reference to the shared state.
Your case fall into what I emphasized (I think). Your created the shared state using std::async
, it is sleeping (so not ready) and you have only one reference to it, so this may be blocking.
it is guaranteed that all tasks are executed when printing out the result,
Only the task assigned last is guaranteed to have been executed. At least, I couldn't find any rules that would guarantee the rest.
whether the tasks would be executed one after another (i.e. the task assignment would be blocking) or not.
Task assignment is generally non-blocking, but it may block in this case - with no guarantee.
[futures.unique_future]
future& operator=(future&& rhs) noexcept;
Effects:
releases any shared state ([futures.state]).
[futures.state]
When an asynchronous return object or an asynchronous provider is said to release its shared state, it means:
if the return object or provider holds the last reference to its shared state, the shared state is destroyed; and
the return object or provider gives up its reference to its shared state; and
these actions will not block for the shared state to become ready, except that it may block if all of the following are true: the shared state was created by a call to std::async, the shared state is not yet ready, and this was the last reference to the shared state.
All of the conditions for potential blocking are true of the task created by std::async
hadn't executed yet.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With