2

Consider the follow code:

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

void func()
{
    std::async(std::launch::async, []{std::this_thread::sleep_for(std::chrono::milliseconds(1000)); });
}

int main()
{

    std::cout << "start " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() << "ms\n";
    func();
    std::cout << "stop  " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() << "ms\n";

    return 0;
}

outputs:

start 18737230ms
stop  18738230ms

We can see that 1 seconds passes before func() returns. However there is no std::future stored from std::async(...); - i.e.: auto f = std::async(...)

This appears to work - but I am wandering what the mechanism is such that this works. If I have a std::future (auto f in my little example) then when it goes out of scope it tidies up the thread - i.e. waits for 1 second and then the thread is disposed of behind the scenes.

A further test:

int main() {

    std::cout << "start " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() << "ms\n";
    std::async(std::launch::async, []{std::this_thread::sleep_for(std::chrono::milliseconds(1000)); });
    std::cout << "stop1 " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() << "ms\n";
    auto f = std::async(std::launch::async, []{std::this_thread::sleep_for(std::chrono::milliseconds(1000)); });
    std::cout << "stop2 " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() << "ms\n";
    return 0;
}

gives:

start 4448133ms
stop1 4449133ms - 1 sec passed
stop2 4449133ms - almost no time passed

So this shows that storing the future, means that the thread runs parallel. Not storing the future means the thread appears to have to run to completion - I guess this is because a temporary future is created and destroyed?

So my conclusion is that you can't just call std::async(...) without storing the std::future if you want it to run in parallel (which is the whole point) - even if you don't plan to use the future.

hmm... I think I have just talked myself into the answer! - but I am not 100% sure I have the reasoning correct - hopefully I have...

John
  • 2,963
  • 11
  • 33
code_fodder
  • 15,263
  • 17
  • 90
  • 167
  • std::async and std::future are really bad. use my library, concurrencpp: https://github.com/David-Haim/concurrencpp – David Haim Sep 18 '20 at 09:17
  • @DavidHaim hm, interesting - just from a quick glance - does this protect variables shared between threads automatically (somehow)? - what is the advantage over std::async otherwise? - and why do you think std::async / future are bad? – code_fodder Sep 18 '20 at 09:22
  • It doesn't protect shared variables, but allows results to be passed safetly between threads and consumed with `co_await`. `std::async` is just a nice wrapper for `std::thread` - not scalable at all. `std::future` also has its problems - destructor is blocking and no way to pull the asynchronous result without blocking. – David Haim Sep 18 '20 at 12:04
  • @code_fodder Note: when it goes out of scope it may **not** tidy up the thread. Two years later, how do you think about it now? – John Jun 08 '22 at 07:57
  • @john - I am not sure what you mean? - when the future goes out of scope it *does* block and tidy up the thread - that is sort of the point of using tasks instead of managing the thread yourself, right? - two years later, I am happily using various methods of concurrency :) – code_fodder Jun 08 '22 at 10:11
  • @code_fodder "tidy up the thread", you mean `join` the thread. I see somewhere says that the thread may be reused later, so the thread may still alive even if the future goes out of scope. – John Jun 08 '22 at 12:06
  • @John well, sort of - I believe std::future/async uses a thread pool to manage the threads, so as you say the thread may or may not be joined.... but as I say, this is for the compiler/implementation to worry about - not for the user of std::future/async – code_fodder Jun 09 '22 at 02:09

1 Answers1

5

If the std::future is created via std::async, the destructor waits for end of the task. This does not mean that the task does not run in parallel - it just waits for the end of the task at the end of scope of variable. Yet it makes usage of std::async without storing a std::future a bit tricky and I would generally recommend storing the future somewhere to avoid nasty surprises. Take a look at page about std::future destructor (emphasis mine):

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.

bartop
  • 9,971
  • 1
  • 23
  • 54
  • 1
    thanks for this, but I am still wandering what happens when you don't store the future. I *think* that means a temp future is returned and destroyed right away - therefore it blocks immediately until the thread ends (so it ends up running synchronously)? – code_fodder Sep 18 '20 at 09:26
  • @code_fodder bascially that's it - when you don't store returned object it is removed right away - so the main thread blocks in the case of `future` – bartop Sep 18 '20 at 09:28
  • tbh the text quoted could be less cryptic. But that's how generally ISO standard rolls: let's describe what happens if we dereference null pointer as a syllogism of phrases in three different paragraphs. Several editions later: let's add clarification into next standard. – Swift - Friday Pie Sep 18 '20 at 09:40
  • @Swift-FridayPie I'd call it know issue with C++ standards so ranting about it makes kinda no sense ;) – bartop Sep 18 '20 at 09:57
  • @bartop "If the std::future is created via std::async, ***the destructor waits for end of the task.***" You mean `std::async(std::launch::async, []{std::this_thread::sleep_for(std::chrono::milliseconds(1000)); });` creates a temporary object of `std::future` and the destructor of the said temporary object waits for the end of the task? Am I right? – John Jun 08 '22 at 02:51
  • @John yes, this is what I meant – bartop Jun 08 '22 at 08:39