Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens when reassigning to a future that is not ready yet

Tags:

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

  • it is guaranteed that all tasks are executed when printing out the result,
  • whether the tasks would be executed one after another (i.e. the task assignment would be blocking) or not.

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?

like image 633
Markus Mayr Avatar asked Jul 01 '16 08:07

Markus Mayr


People also ask

What are the 5 stages of behavior change?

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.

What is the contemplation stage?

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)

Is the stage of change when the person is not considering making any changes?

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.

What is the relapse stage of change?

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.


2 Answers

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.

Why do you get a 18 seconds execution time?

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.

Standard details on 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.

like image 101
Holt Avatar answered Sep 17 '22 08:09

Holt


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;

  1. Effects:

    releases any shared state ([futures.state]).

[futures.state]

  1. 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.

like image 42
eerorika Avatar answered Sep 19 '22 08:09

eerorika