C++, as a language, has had a resurgence over the past few years. Personally, I think there’s a couple of reasons for this. Embedded programming and IoT devices have become more common development targets over the past few years, and C++ has become easier to use as the standard library and overall language syntax has become more powerful (thanks to auto!). Furthermore, programming with generic types has become much more common, making C++ templates a bit easier to understand for those just getting started.
You may also like: How Can We Control/Schedule Execution of Threads in C, C++?
One of the key improvements in the standard library is task-based programming. We’ve had thread support forever, but task-based programming has been evolving over the past few years. At this point, you have a robust task-based option with std::async(.). You should use it! It’s robust, well designed, and performant. Unless you shouldn’t. How can you tell?
std::async implementations. So std::async(.) doesn’t guarantee a separate thread first of all. What it does guarantee is asynchronous processing of a submitted function (or lambda expression). It gives an easy way to provide a return value (sure, you can share a variable to indicate state between threads, but enjoy synchronizing that on your own). But how can it do this if it’s not on a separate thread?
What does this do? Not much. (There are options you can use to force thread behavior too, but they’re not important to the more common use of std::async(.)). The important things to note are the third and fourth lines. So std::async(.) is invoked with only one argument, to begin with.
When you do this, you essentially give the runtime the option to either invoke on a separate thread immediately or invoke when either get(.) or wait(.) are called on the return future object. And the runtime will almost always select the latter option.
Does this mean that async is a lie? Well, yes and no. The code in task(.) is executing asynchronously — though it executes when the get(.) method is called, it does execute on another thread. So it is asynchronous, but it executes just-in-time.
It also usually executes on a thread maintained in a thread pool for efficiency — which is a good thing. This is perfect for specific, delineated tasks like reading from a file or waiting on a network response. It’s not so great for things that execute repeatedly over time.
If you have a task that really needs its own thread, like a network polling, you need to think carefully about how you implement it. You could, for example, implement the polling in a task that contains a loop — but this will remove a thread from the thread pool used by std::async(.) indefinitely (or until the polling loop ends).
You could also execute the polling by looping in the main thread and executing tasks. But this will essentially be logically equivalent to executing the tasks on a single thread. The work in the task will execute on a separate thread, but it will usually execute only when the future object is referenced via wait(.) or get(.).
If you’re using all the resources of a thread — you should use one. The first case steals a thread from the thread pool — something you don’t want to do. You might need these later. The second case is just a more complicated synchronous execution. The best solution? Just create a dedicated thread and use it.
If you’re going to use a thread for a long period of time, create one and use it. If you just need to execute something off the main thread, like waiting for a network response or reading a large file, use a task.