Confusion about threads launched by std::async with std::launch::async parameter
Asked Answered
M

3

49

I am a little bit confused by the std::async function.

The specification says:

asynchronous operation being executed "as if in a new thread of execution" (C++11 §30.6.8/11).

Now, what is that supposed to mean?

In my understanding, the code

std::future<double> fut = std::async(std::launch::async, pow2, num);

should launch the function pow2 on a new thread and pass the variable num to the thread by value, then sometime in the future, when the function is done, place the result in fut (as long as the function pow2 has a signature like double pow2(double);). But the specification states "as if", which makes the whole thing kinda foggy for me.

The question is:

Is a new thread always launched in this case? I hope so. I mean for me, the parameter std::launch::async makes sense in a way that I am explicitly stating I indeed want to create a new thread.

And the code

std::future<double> fut = std::async(std::launch::deferred, pow2, num);

should make lazy evaluation possible, by delaying the pow2 function call to the point where i write something like var = fut.get();. In this case the parameter std::launch::deferred, should mean that I am explicitly stating, I don't want a new thread, I just want to make sure the function gets called when there is need for it's return value.

Are my assumptions correct? If not, please explain.

Also, I know that by default the function is called as follows:

std::future<double> fut = std::async(std::launch::deferred | std::launch::async, pow2, num);

In this case, I was told that whether a new thread will be launched or not depends on the implementation. Again, what is that supposed to mean?

Miyasawa answered 12/6, 2015 at 18:55 Comment(7)
"as if" means that it can theoretically reuse an existing thread (e.g., in a thread pool) as long as the behavior is indistinguishable. In practice, very few (if any) implementation does this because "as if a new thread" requires you to destroy and recreate all thread-local variables.Mair
@Mair or implement (heavy) coroutine-local variables. Have each thread get a default coroutine, and thread_local is coroutine local. The async can create a coroutine, that coroutine can be plopped into another thread and run. Ie, emulate threading (with coroutines) on top of a OS-provided thread model?Stedman
I heed std::async is broken, is that true?Gearbox
I don't get why std::async running in new thread should suprise you? this is what asynchronous action means..Unbutton
@DavidHaim asynchronous action does not mean "on another thread". It just means happens without being in sync with another action (the sync in this case being when the other action finishes). Which could mean on another thread... or seventy-twelve other things.Highams
@TheFloatingBrain Where did you 'hear'? And define "broken". It looks fine to me.Haggard
@Haggard I think here on stack overflow and possibly in an article I don't remeber exactly where or exactly waht the bug was, but I think the person indicated there was a problem with it/it's standard that caused to to not function properly and or have a malformed behavior with some sort of problem.Gearbox
S
68

The std::async (part of the <future> header) function template is used to start a (possibly) asynchronous task. It returns a std::future object, which will eventually hold the return value of std::async's parameter function.

When the value is needed, we call get() on the std::future instance; this blocks the thread until the future is ready and then returns the value. std::launch::async or std::launch::deferred can be specified as the first parameter to std::async in order to specify how the task is run.

  1. std::launch::async indicates that the function call must be run on its own (new) thread. (Take user @T.C.'s comment into account).
  2. std::launch::deferred indicates that the function call is to be deferred until either wait() or get() is called on the future. Ownership of the future can be transferred to another thread before this happens.
  3. std::launch::async | std::launch::deferred indicates that the implementation may choose. This is the default option (when you don't specify one yourself). It can decide to run synchronously.

Is a new thread always launched in this case?

From 1., we can say that a new thread is always launched.

Are my assumptions [on std::launch::deferred] correct?

From 2., we can say that your assumptions are correct.

What is that supposed to mean? [in relation to a new thread being launched or not depending on the implementation]

From 3., as std::launch::async | std::launch::deferred is the default option, it means that the implementation of the template function std::async will decide whether it will create a new thread or not. This is because some implementations may be checking for over scheduling.

WARNING

The following section is not related to your question, but I think that it is important to keep in mind.

The C++ standard says that if a std::future holds the last reference to the shared state corresponding to a call to an asynchronous function, that std::future's destructor must block until the thread for the asynchronously running function finishes. An instance of std::future returned by std::async will thus block in its destructor.

void operation()
{
    auto func = [] { std::this_thread::sleep_for( std::chrono::seconds( 2 ) ); };
    std::async( std::launch::async, func );
    std::async( std::launch::async, func );
    std::future<void> f{ std::async( std::launch::async, func ) };
}

This misleading code can make you think that the std::async calls are asynchronous, they are actually synchronous. The std::future instances returned by std::async are temporary and will block because their destructor is called right when std::async returns as they are not assigned to a variable.

The first call to std::async will block for 2 seconds, followed by another 2 seconds of blocking from the second call to std::async. We may think that the last call to std::async does not block, since we store its returned std::future instance in a variable, but since that is a local variable that is destroyed at the end of the scope, it will actually block for an additional 2 seconds at the end of the scope of the function, when local variable f is destroyed.

In other words, calling the operation() function will block whatever thread it is called on synchronously for approximately 6 seconds. Such requirements might not exist in a future version of the C++ standard.

Sources of information I used to compile these notes:

C++ Concurrency in Action: Practical Multithreading, Anthony Williams

Scott Meyers' blog post: http://scottmeyers.blogspot.ca/2013/03/stdfutures-from-stdasync-arent-special.html

Sharpnosed answered 11/9, 2015 at 6:45 Comment(3)
Just to clarify, if you change the plain calls of std::async to auto t1 = std::async(...); in your example, the block time will be only about 2 seconds, right?Oriane
@AdamHunyadi if you mean the total block time - yes, because three different threads would be sleeping simultaneously.Lobby
Thanks! I had no idea why my calling thread was being blocked when I forced an async launch.Unnerve
P
4

I was also confused by this and ran a quick test on Windows which shows that the async future will be run on the OS thread pool threads. A simple application can demonstrate this, breaking out in Visual Studio will also show the executing threads named as "TppWorkerThread".

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

using namespace std;

int main()
{
    cout << "main thread id " << this_thread::get_id() << endl;

    future<int> f1 = async(launch::async, [](){
        cout << "future run on thread " << this_thread::get_id() << endl;
        return 1;
    });

    f1.get(); 

    future<int> f2 = async(launch::async, [](){
        cout << "future run on thread " << this_thread::get_id() << endl;
        return 1;
    });

    f2.get();

    future<int> f3 = async(launch::async, [](){
        cout << "future run on thread " << this_thread::get_id() << endl;
        return 1;
    });

    f3.get();

    cin.ignore();

    return 0;
}

Will result in an output similar to:

main thread id 4164
future run on thread 4188
future run on thread 4188
future run on thread 4188
Piegari answered 16/3, 2017 at 11:8 Comment(2)
should remove f1.get() f2.get() f3.get()Praemunire
that will make the functions in this example run on different threads, but if you revise this sample to spawn say 100,000 futures and then break in the async function periodically you will see in the Visual Studio thread debugger that the same few threads are being re-used.Piegari
H
1

That is not actually true. Add thread_local stored value and you will see, that actually std::async run f1 f2 f3 tasks in different threads, but with same std::thread::id

Halflength answered 9/2, 2018 at 12:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.