Today I would like to introduce the C++ threaded high-level APIs: std::promise, std::future, std::packaged_task and std::async. The content of this article can be condensed into the following diagram.

where std::promise and std::future are synchronisation channels between threads. The std::packed_task class template is an adapter for a function or a function object. It wraps the return value of a function in std::future, allowing us to easily execute any function with std::thread. The std::async function is equivalent to the sum of std::packaged_task and std::thread.
I will then introduce each API in turn.
- std::promise and std::future
- std::packaged_task
- std::async
std::promise and std::future
The std::promise<T> and std::future<T> class templates are defined in the <future> header file. Together they form a synchronisation channel. Where std::promise is the sender and std::future is the receiver. The specific usage is as follows.
In the above example, we first create a std::promise<int> object. Where the int pattern argument means that this synchronous channel will pass an int object. Next, we call p.get_future() to get the receiver. We then pass 42 with p.set_value(42) and get the object with f.get().
Next, let’s add a thread.
In this example, we hand over std::promise<int> to another thread, which passes the int object, and the main thread reads the int object with f.get(). If the master thread executes to f.get() first, the master thread will wait for the other thread until p.set_value(42) is finished.
Also, this sync channel can only be used once. If we call p.set_value(...) or f.set_value(...)' multiple times or f.get() they will throw std::future_error exceptions.
Strictly speaking, calling
std::future<T>::get()orstd::promise<T>::set_value()multiple times is an Undefined Behavior. However, the C++ standard encourages C++ authors to throw instd::future_erroras an exception.
|
|
wait member function
On the receiver side we can split the receiver ‘wait’ and ‘read’ into two steps. std::future<T> has three member functions.
void wait(): waits until the object is ready to be read.future_status wait_for(const std::chrono::duration<... > &): Wait until the object is ready to be read or until all the wait time has been used.future_status wait_until(const std::chrono::time_point<... > &): Wait until the object can be read or the deadline is reached.
The return values for the last two functions can be.
future_status::deferred: Thisstd::futureobject corresponds to the sending end of an inert evaluation (see thestd::asyncparagraph).future_status::ready: The object is ready to be read.future_status::timeout: Waiting for timeout.
For example, if another thread takes a while to produce a return value, and we want the main thread to print something while waiting, we can use the wait_for member function.
|
|
Another situation is where we just want to synchronise the point in time when the execution starts with std::promise and std::future, and we don’t really want to send an object. In this case we can use the wait member function.
|
|
Exception handling
We can also use std::promise to send an Exception. If the sender wants to send an “exception” to the receiver, we can call std::promise<T>::set_exception. When the receiver calls std::future<T>::get(), the get() function will again throw the exception.
|
|
It is worth noting that p.set_exception(...) has the argument type std::exception_ptr, so we cannot pass the std::runtime_error object instance directly. We must first throw the exception with a throw statement, then get std::exception_ptr with std::current_exception() in the catch clause and call set_exception to send the exception.
std::shared_future
Although the purpose of std::future and std::promise is to pass objects between threads. However, std::future objects cannot be manipulated by multiple threads at the same time. For example, in the following example, the threads t1 and t2 call f.get() at the same time. However, because the get() member function itself is not entirely Thread-safe, the following code will have undefined behaviour (and generate a Segmentation Fault on my machine).
|
|
The solution is also very simple. If we want two (or more) threads to be receivers at the same time, we should first call the share() member function of std::future. This will convert the std::future object to a std::shared_future object. We then copy the std::shared_future object so that t1 and t2 threads each have a copy.
|
|
std::packaged_task
The std::packed_task class template is also defined in the <future> header file. Its purpose is to act as an adapter between a ‘function’ or ‘function object’ and std::thread. In general, before considering multi-threaded execution, we would define a function as
|
|
However, if the above function is used as the first argument of the std::thread construct, the return value of the function will be ignored by std::thread. In addition, if an exception is thrown to the above function, std::thread will simply terminate the entire program. To resolve the interface gap, the C++ standard library defines a template for the std::packaged_task class. It defines a get_future member function to return a std::future object that can receive a return value. It also defines an operator() member function to call the original function.
A simplified implementation of std::packaged_task is as follows (for reference only, the actual implementation is more complex).
|
|
The following is how std::packaged_task is used.
Without changing the compute function, we wrap the compute function in std::packaged_task. After calling task(3, 4), the return value can be obtained by calling f.get().
We can then add the thread.
|
|
The above code replaces the original task(3, 4) with the code that creates the thread. Since std::packaged_task is a non-copyable class, we must transfer the std::packaged_task object to the std::thread construct with std::move(task). Next, we call t.detach() to avoid std::thread’s destructor calling std::terminate. On the other hand, the construct of std::thread will call std::packaged_task::operator() and execute the compute function for us at another thread. When compute finishes, the main thread can receive the returned value via f.get().
std::async
Finally, I would like to introduce the std::async function sample. When using std::async, we must pass in a function or function object with the arguments needed to call the function. std::async will call the function we pass in at a certain point in time. The caller of std::async can read the return value of the incoming function via the std::async returned std::future<T> object.
There are two different execution strategies for std::async.
std::launch::async: creates an execution thread, executes the specified work and returns astd::future<T>object.std::launch::deferred: Returns astd::future<T>object directly and defers the specified work to the call point ofstd::future<T>::get().
For example, the previous example of std::packed_task could also be rewritten as
The above code’s std::async creates a thread to execute compute(3, 4). The main thread gets the return value of compute(3, 4) from f.get().
A simple std::async(std::launch::async, ...) works as follows (for reference only, the actual implementation is more complex).
|
|
On the other hand, the std::launch::deferred execution policy does not create a new execution thread. It works by maintaining an additional state inside the std::future<T> object. When the user calls std::future<T>::get, the get member function will execute the incoming function. If no one calls std::future<T>::get, std::async(std::launch::deferred, ...) will not execute the incoming function.
For example, in std::launch::deferred mode, the following code must print the first line before the second line. If you delete the f.get() line, the whole program must not print the second line.
|
|