Weekly C++ 8- std::thread (II)

Well, it is time to continue our thread discussion. In this post, I am going to write about some supporting constructs that I believe you need to know while developing multithreaded applications for C++. As you may guess, most of the multithreading background information is given in my previous post, so if you have not read that post, I strongly suggest to check that first which can be accessed with below link:

Weekly C++ 7- std::thread (I)

Initially, I was planning to continue with synchronization constructs, but then decided to cover other constructs and devote a seperate post for that topic. Otherwise, the content of this post will be very much and hard to track. The topics that I will cover in this post are async, futures, promises, packaged_tasks and finally atomics. So, let us start.

std::async:

async is the name of template functions that are provided in ‘<future>’ library. This methods are used to execute provided functions asynchronously (usually implemented as seperate thread that is not visible to developer) and returns the result (and status) in an instance of std::future which I will explain in next section. So, it can be seen as a high level construct for std::threads. As you may guess, you can pass functions, function pointers, function objects and lambda function to async function as well as parameters. Parameters expected by corresponding function can be passed to std::async() as arguments. Following sample code from its reference page summarizes its usage:

In addition to functions and parameters, std::async also accepts a launch policy parameter which can take three values:

  • std::launch::async
    • Guarantees the asynchronous behaviour i.e. passed function will be executed in seperate thread.
  • std::launch::deferred
    • Non-asynchronous behaviour i.e. function will be called when other thread will call get() on future object to access the shared state.
  • std::launch::async | std::launch::deferred
    • Its the default behaviour. With this launch policy it can run asynchronously or not depending on the load on system. But we have no control over it.

I am adding another sample code (from one of reference that I gave at the end of post) which I believe will help you to understand the std::async():

It should be noted that, the get() API of std::future object will block the current thread till corresponding task function completes its processing. Moreover, std::future object returned from std::async()  will also block when its destructor is called as illustrated below:

You can find more details about this in Scott’s blog post.

Now, it is time to look at std::future.

std::future & std::promise:

Another construct that come with C++ 11 is std::future. To use that, you should include ‘<future>’ library. In fact, it is used with async() methods as we saw in previous section as well as std::packaged_task and std::promise. So what is the use of std::future? You may guess that it is somehow related with returning values from threads (at least ones created by async) by looking at the sample given above.

Traditionally, you need to share data with corresponding thread via synchronization constructs such as mutexes which are used to protect it. Additionally, you need to also come up with a mechanisms to inform the caller about the result of operation (usually via std::conditional_variable). So what is alternative? Well, you may use std::future for this purpose.

It is again a class template and its object stores the value that will be provided by callee. In other words, it internally stores the value that will be assigned by callee (which of course protected by underlying mechanisms) and then let caller access this value via .get() API. It should be noted that if .get() method is called before callee set the value then this call will block the current thread till value is available.

Similarly, std::promise is also a class template and its object state a promise to set the value in future. So, each std::promise object has an associated std::future object that will give the value once it is set by the std::promise object. A std::promise object shares data with its associated std::future object and provides an interface for callee to set the value. The following diagram that took from the reference that I gave at the end, summarizes their relationship and how they are working together:

std::promise and std::future

A std::promise object represents an object whose value/error can be set. On the other hand, a std::future represents the result of a std::promise object. In other words, the caller of the future value/error will first create a std::promise and return one or more std::futures from it. Then it passes it to callee and the callee will either set a value or error on the std::promise objects passed. Afterall, all std::future objects can be checked to see if std::promise got resolved or rejected. Sample code given below illustrates this simple relationship:

If std::promise object is destroyed prior to setting the value and caller calls the get() API on associated std::future object, then an exception will be thrown. Another usage that might be required is multiple return values in which multiple std::promise objects can be passed to thread and the caller can obtain values from associated std::future objects.

Another good explanation is given by Rainer Grim in this post in which he summarizes these two constructs as follow:

A std::promise permits:

  • To set a value, a notification or an exception. That result can in addition delayed be provided by the promise.

A std::future permits:

  • To pick up the value from the promise,
  • To asks the promise, if the value is available,
  • To wait for the notification of the promise. That waiting can be done with a relative time duration or an absolute time point. => Replacement for condition variables,
  • To create a shared future (std::shared_future).

Now let us look at another sample code provided in given post which also illsutrate std::future and std::promise usages:

There is also another construct similar to std::future which is std::shared_future. Its description is best summarized with following description given in std::shared_future API document:

The class template std::shared_future provides a mechanism to access the result of asynchronous operations, similar to std::future, except that multiple threads are allowed to wait for the same shared state. Unlike std::future, which is only moveable (so only one instance can refer to any particular asynchronous result), std::shared_future is copyable and multiple shared future objects may refer to the same shared state. In fact, a shared_future may be used to signal multiple threads simultaneously, similar to std::condition_variable::notify_all().

Let us see how it is used in following sample code:

std::packaged_task:

Another construct related with std::future, std::promise and async is std::packaged_task<>. It is a class template and represents a asynchronous task which can be invoked asynchronously. Its return value or exception thrown is stored in a shared state which can be accessed through std::future objects. It encapsulates:

  • A callable entity i.e either standard function, member function pointer, lambda function or function object,
  • A shared state that stores the value returned or thrown exception by associated callback.

So you can request std::future object from std::packaged_task and then query the result via this instance as illustrated in following sample code which also illustrate possible usage of std::packaged_task with different callables:

In above sample, you should note that packaged_task instance should be moved, in other words, it is not copy-able. So usually, future object should be obtained before it is moved to corresponding thread.

At this point, you may wonder about the differences between packaged_task and async method. Well, it is a good question to ask 🙂  Let me try to summarize the differences as follow:

  • First of all, async immediately start to execute, but you need to invoke packaged_task  instance for execution,
  • Closely related with previous item, you need to invoke packaged_task instance before calling get() API on obtained std::future instance. Otherwise, your program will freeze and never become ready,
  • You can not specify the thread that async function will run. However, you can pass (move in fact as ween above) std::packaged_task to other threads that will execute given task,
  • std::packaged_task can be assumed as a lower level feature for implementing std::async (which is why it can do more than std::async if used together with other lower level stuff, like std::thread). In other words, a std::packaged_task is a std::function linked to a std::future and std::async that wraps and calls a std::packaged_task (possibly in a different thread).

Well that’s all for std::packaged_task. N

Atomics:

If you read my previous post, you should note that we should take care of the data that we are going to share with multple threads and use synchronization constructs for this purpose. Although we will cover these constructs in other posts, let us look at another library provided with STL that allows us to update data from multiple threads without using such constructs. These are called Atomic Types. As described in std::atomic library reference page, these types encapsulate a value whose access is guaranteed to not cause data races and can be used to synchronize memory accesses among different threads. Well, before delving into more details, let us look at an example code where we would like to update some members from different threads:

It should be obvious that above class is not suitable for multithreaded environment, but it can be safe with using constructs that I desribed above such as mutexes. Let us look at, how we can make this code safe employing atomic types.

Atomic types are used through a template class which is called std::atomic. You should include <atomic> header file to use this library. You can provide the data type that should be used for atomic operation (there are also various type alias for known data types). The underlying syncronization mechanism depends on corresponding library implementation and type that provided to this class. For integral types such as int, long, float, bool, etc. there will be much less overhead than mutexes and can be used as lock-free (in fact underlying mechanism depends on lock-free atomic CPU instructions). However, if you use other types for atomics, mutexes will probably be employed for synchronization and use of atomics provides no performance gain except simplier syntax. The lock-freeness can be checked via is_lock_free() API. Following sample from https://en.cppreference.com/w/cpp/atomic/atomic/is_lock_free is a good example:

The functions provided by std::atomic class are store() and load() which automically update and obtain the contents of the std::atomic type. Another function is exchange(), that sets the atomic variable to a new value and returns the value held previously. Finally, there are also two functions compare_exchange_weak() and compare_exchance_strong() that performs atomic exchange only if the value is equal to the provided parameter. Last three functions are usually used to implement lock-free algorithms (hopefully, I will also write about this topic). There are also specialized functions for for all integral data types such as operators ++, –, fetch_add, fetch_sub (full list can be found at https://en.cppreference.com/w/cpp/atomic/atomic) which are obvious from their names 🙂

Now let us make our class thread safe using std::atomic:

In my opinion, main advantages of atomics is its performance advantage (for integral types) and readibility. Hence, I strongly advice you to use std::atomics, if you need to perform atomic operations on integral types.

There are also some additions to atomics with C++ 20. It is now possible to also use shared and weak smart pointers with atomics library as explained in https://en.cppreference.com/w/cpp/atomic/atomic.

By the way, there is also another topic closely related with atomics: memory ordering (https://en.cppreference.com/w/cpp/atomic/memory_order) which I have not mentioned. As described in its reference page it specifies how regular, non-atomic memory accesses are to be ordered around an atomic operation. So if you would like to learn more details about this topic, you can out the references that I have added.

See you in my next post.

References:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.