This is my second article on multi-threaded programming. I highly recommend you to read the previous article before proceeding any further.
Concurrent programming, Parallel programming and Asynchronous programming are often spoken about in the same breath. However they aren’t necessarily similar. It is important to know their distinction and how multi-threading can facilitate their implementation.
In concurrent programming, tasks that are seemingly executed in parallel are not in-fact executed at the same time. Here, the executioner (eg:-OS) juggles between multiple tasks. When a portion of a certain task is completed, it’s state is saved and is put to sleep. Executioner then picks up the next task and follows the same procedure. This continues until all the tasks are completed.
In parallel programming, tasks that are executed in parallel are truly executed in parallel via multiple threads of execution. Each logical CPU executes a unique task and all of them run at the same time.
In asynchronous programming, there is often a background task that needs to executed by the main program but the main program does not want to wait until this task finishes execution. The background task could be external, ie, it may be carried out by an external application. In this way, the main program is not blocked and therefore can continue execution while the external application runs the background task.
There are a few different ways to write multi-threaded code in C++. Let us start with the low level constructs and slowly proceed towards the higher level constructs.
Creating a thread manually is the traditional way to write multi-threaded code. We can then pass to this thread, a reference to a function. It is important to either join() or detach() this newly created thread from the parent thread. Failure to do so will result in a run-time error when the child thread returns. If some value needs to be returned from the child thread to the parent thread, we can use constructs like std::promise and std::future. Both std:: promise and std::future are objects that give us access to a shared state between threads. While std:: promise lets us write to the shared state, std::future lets us read from the shared state. So what we need to do is to create an std::promise object in the parent thread and pass it to the newly created child thread. We can get the std::future object referring to the same shared state as the std::promise object by calling the get_future() method of the std::promise object. Please note std::promise is not copyable. So it cannot be passed by value to the child thread. It has to be either passed by reference or moved (ie transfer ownership via std::move).
Following example illustrates the use of these constructs.
Programs typically use what is known as ‘thread-pools’ to perform tasks asynchronously. A thread-pool can be thought of as an object that holds a queue of jobs and a certain number of worker threads that pops jobs off this queue and execute them. Whenever a task needs to be completed, the main thread (or any other thread that uses this thread-pool) wraps this task into a job and adds it to the thread-pool’s queue along with an optional callback. A worker thread that executes this job invokes this callback to let the main thread know once it completes the job.
The C++ standard library does not provide a built-in thread-pool unfortunately. Therefore, it is up to us to implement our own thread-pool, if need be. But I am pretty sure there are libraries out there that provide well equipped thread-pools that could even be dynamic in nature. Dynamic thread-pools generally have a built-in scaling mechanism to create additional threads based on demand.
Following example shows a simple implementation of a thread-pool class.
std:: packaged_task is a higher level construct than std::thread that makes it easier to write multi-threaded code. It can wrap a callable target and provides an std::future to fetch the returned value of the target. Note that the packaged task can either be invoked from the same thread or from a different thread but in either case, it needs to be invoked explicitly. The following example would hopefully clarify any doubts you may still have.
You can read more about std::packaged_task here.
std::async is an even higher level construct than std::packaged_task that further simplifies the creation of multi-threaded code. Similar to an std::packaged_task, std::async allows a callable target to be specified to it and provides an std::future to fetch the returned value. Unless std::launch::async is specified in its constructor, the callable target is most likely invoked from within the same thread.
You can read more about std::async here.