It is loosely related to this question: Are std::thread pooled in C++11?. Though the question differs, the intention is the same:
Question 1: Does it still make sense to use your own (or 3rd-party library) thread pools to avoid expensive thread creation?
The conclusion in the other question was that you cannot rely on std::thread
to be pooled (it might or it might be not). However, std::async(launch::async)
seems to have a much higher chance to be pooled.
It don't think that it is forced by the standard, but IMHO I would expect that all good C++11 implementations would use thread pooling if thread creation is slow. Only on platforms where it is inexpensive to create a new thread, I would expect that they always spawn a new thread.
Question 2: This is just what I think, but I have no facts to prove it. I may very well be mistaken. Is it an educated guess?
Finally, here I have provided some sample code that first shows how I think thread creation can be expressed by async(launch::async)
:
Example 1:
thread t([]{ f(); });
// ...
t.join();
becomes
auto future = async(launch::async, []{ f(); });
// ...
future.wait();
Example 2: Fire and forget thread
thread([]{ f(); }).detach();
becomes
// a bit clumsy...
auto dummy = async(launch::async, []{ f(); });
// ... but I hope soon it can be simplified to
async(launch::async, []{ f(); });
Question 3: Would you prefer the async
versions to the thread
versions?
The rest is no longer part of the question, but only for clarification:
Why must the return value be assigned to a dummy variable?
Unfortunately, the current C++11 standard forces that you capture the return value of std::async
, as otherwise the destructor is executed, which blocks until the action terminates. It is by some considered an error in the standard (e.g., by Herb Sutter).
This example from cppreference.com illustrates it nicely:
{
std::async(std::launch::async, []{ f(); });
std::async(std::launch::async, []{ g(); }); // does not run until f() completes
}
Another clarification:
I know that thread pools may have other legitimate uses but in this question I am only interested in the aspect of avoiding expensive thread creation costs.
I think there are still situations where thread pools are very useful, especially if you need more control over resources. For example, a server might decide to handle only a fixed number of requests simultaneously to guarantee fast response times and to increase the predictability of memory usage. Thread pools should be fine, here.
Thread-local variables may also be an argument for your own thread pools, but I'm not sure whether it is relevant in practice:
- Creating a new thread with
std::thread
starts without initialized thread-local variables. Maybe this is not what you want. - In threads spawned by
async
, it is somewhat unclear for me because the thread could have been reused. From my understanding, thread-local variables are not guaranteed to be resetted, but I may be mistaken. - Using your own (fixed-size) thread pools, on the other hand, gives you full control if you really need it.
std::async(launch::async)
seems to have a much higher chance to be pooled." No, I believe itsstd::async(launch::async | launch::deferred)
that may be pooled. With justlaunch::async
the task is supposed to be launched on a new thread regardless of what other tasks are running. With the policylaunch::async | launch::deferred
then the implementation gets to choose which policy, but more importantly it gets to delay choosing which policy. That is, it can wait until a thread in a thread pool becomes available and then choose the async policy. – bames53std::async()
. I'm still curious to see how they support non-trivial thread_local destructors in a thread pool. – bames53launch::async
then it treats it as if it were onlylaunch::deferred
and never executes it asynchronously - so in effect, that version of libstdc++ "chooses" to always use deferred unless forced otherwise. – doug65536std::async
could have been a beautiful thing for performance - it could have been the standard short-running-task execution system, naturally backed by a thread pool. Right now, it's just astd::thread
with some crap tacked on to make the thread function be able to return a value. Oh, and they added redundant "deferred" functionality that overlaps the job ofstd::function
completely. – doug65536