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();
}
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::future
s 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::future
s 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.