0

In the first example code, all tasks are launched successfully without any issues. However, in the second example code, only the first task is launched, and the program waits there without executing the remaining lines of code. It seems even if when the the functors of class (A,B,C,D) don't return anything (void), we need to define objects of std::future type and I don't understand why!

// example #1
int main()
{
    A a("A");
    B b("B");
    C c("C");
    D d("D");
    Controller controller("Controller");

    // Resources shared between threads
    SharedResource sharedResource;
    ControllerResource controllerResource;

    std::future<void> taskA = std::async(std::launch::async, a, std::ref(sharedResource));
    std::future<void> taskB = std::async(std::launch::async, b, std::ref(sharedResource));
    std::future<void> taskC = std::async(std::launch::async, c, std::ref(sharedResource));
    std::future<void> taskD = std::async(std::launch::async, d, std::ref(sharedResource));
    std::thread thController(controller, std::ref(controllerResource), std::ref(sharedResource));
    thController.join();
}
// example #2
int main()
{
    A a("A");
    B b("B");
    C c("C");
    D d("D");
    Controller controller("Controller");

    // Resources shared between threads
    SharedResource sharedResource;
    ControllerResource controllerResource;

    std::async(std::launch::async, a, std::ref(sharedResource));
    std::async(std::launch::async, b, std::ref(sharedResource));
    std::async(std::launch::async, c, std::ref(sharedResource));
    std::async(std::launch::async, d, std::ref(sharedResource));

    std::thread thController(controller, std::ref(controllerResource), std::ref(sharedResource));
    thController.join();
}
Sami
  • 513
  • 4
  • 11
  • 2
    Because if you let the future go out of scope (also by not using it), the future will synchronize with the asynchronous task. So all you std::async lines are now fancy synchronous calls. This is a very valuable property, I use it a lot if objects start member functions then make sure the future is a member variable, this way destructing the object automatically syncs with the thread (from std::async) – Pepijn Kramer Feb 22 '23 at 13:58
  • 1
    Side note: you might want to learn about [lambda functions](https://en.cppreference.com/w/cpp/language/lambda) and shared_ptr when passing shared resources. E.g. `std::shared_ptr resource =... ; std::future f = std::async(std::launch::async, [resource]{ do_something_with(resource); });`. The [resource] captures the shared pointer by value (copy) and will extend the lifecycle of resource to the duration of the thread that uses it – Pepijn Kramer Feb 22 '23 at 14:02
  • @PepijnKramer Would you please clarify more on your first comment? I am still confused – Sami Feb 22 '23 at 14:07
  • 1
    The whole purpose of std::future is to synchronize with an asynchronous taks (whether or not it returns a void). When future.get() returns you know the task is done, regardless of how it was executed. The same is true for the future object's destructor will wait for the task to be done (also if you did not call get). For reference that behavior is described here : https://en.cppreference.com/w/cpp/thread/future. – Pepijn Kramer Feb 22 '23 at 14:16
  • 2
    If you don't assign the result of std::async to a future, it will still create one. And this instance will be destroyed on the same line it is created... and will then wait for the task to complete. – Pepijn Kramer Feb 22 '23 at 14:18
  • Thanks @PepijnKramer for the explanations! It makes sense now. I actually studied std::async in en.cppreference.com and it was mentioned about the situation where we don't create an object of type std::future. – Sami Feb 22 '23 at 19:33

1 Answers1

1

std::async when passed a std::launch::async returns std::future objects that block on the thread finishing on destruction.

This is because having loose threads in a C++ program makes avoiding undefined or unspecified behaviour nearly impossible. Threads running after the end of main (or during/after exit or similar) should be avoided at nearly all costs.

To make std::async usable without spewing undefined behavior, the std::future that it returns behaves in a less than ideal manner. And when you don't store it, it is destroyed on the same line you call std::async.

This destruction stops the main thread until the worker thread you just created finishes. Which is far less than ideal.

[[nodiscard]] warnings for sufficiently advanced C++ compilers can help here.

std::async(std::launch::async, a, std::ref(sharedResource));

this line launches a thread that runs a(sharedResource), but then the std::future std::async returns is cleaned up. That cleanup blocks until the thread finishes. So this is far less async than you intended.

The intended use of std::async is something like taking a problem, dividing it up into (# CPU) sub problems, making a vector of std::futures of size (# CPU-1) and populating them with calls to std::async each working on a subproblem, then working on the final sub problem in the main thread. Finally, you block on all of the std::futures in the vector, and collect the results.

When used like this, std::async is a very useful tool.

If you want more advanced threading usage, you may have to write your own framework. C++ provides threading primitives. Direct use of these primitives without extreme discipline will lead to unmaintainable buggy code in my experience.

Part of this is because in general correct multithreaded code is Hard, as in not tractable. This is true in almost every language. In C++, they give you access to near bare-metal threading with a very expressive memory model of how inter-thread communication works. This makes it harder to write correct threading code, but only in a linear manner; most non-C++ imperative languages also have insanely broken use of threads and a memory model that basically throws its hands up and says "what the single language implementation does is what it does".

But that is a rant for a different time.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Thanks for this great and comprehensive explanations. Now that makes sense why with std::thread we have to have either detach or join methods but with std::async the destructor of std::future will wait to finish the tasks and then do the cleanup before main exits. Also I think std::async could be better because if we have detach with std::thread, chances are threads are still running after main exits, am I right? – Sami Feb 22 '23 at 19:50
  • 1
    @Sam You should only use `std::async` for "do 4 things in parallel" simple tasks. You should almost never detach a `std::thread`; instead, the thread should be owned by some pool who is in charge of `.join`ing it eventually. The only times you detach a `std::thread` safely is (a) a program that literally never ends, and (b) you use `make_ready_at_thread_exit` or equivalent to punt the synchronization to some other means. – Yakk - Adam Nevraumont Feb 22 '23 at 19:53
  • Thanks. You are too knowledgeable :) – Sami Feb 22 '23 at 19:56