Tasks in the form of promises and futures have in C++11 an ambivalent reputation. On the one hand, they are much easier to use than threads or condition variables; conversely, they have a significant deficiency. They can not be composed. C++20 will overcome this deficiency.
Before I write about extended futures, let me say a few words about the advantages of tasks over threads.
The higher abstraction of tasks
The key advantage of tasks over threads is that the programmer has only to think about what has to be done and not how – such as for threads – it has to be done. The programmer gives the system some job to perform, and the system ensures that the job will be executed by the C++ runtime as smartly as possible. That can mean the job will be executed in the same process, or a separate thread will be started. That can mean that another thread steals the job because it is idle. Under the hood, a thread pool accepts the job and distributes it intelligently. If that is not an abstraction?
I have written a few posts about tasks in the form of std::async, std::packaged_task, std::promise, and std::future. The details are here tasks: But now the future of tasks.
The name extended futures is quite easy to explain. Firstly, the interface of std::future was extended; secondly, there are new functions for creating compensable special futures. I will start with my first point.
std::future has three new methods.
An overview of the three new methods.
- The unwrapping constructor unwraps the outer future of a wrapped future (future<future<T>>).
- The predicate is_ready returns whether a shared state is available.
- The method then attaches a continuation to a future.
At first, to something quite sophisticated. The state of the future can be valid or ready.
valid versus ready
- A future is valid if the futures has a shared state (with a promise). That has not to be because you can default-construct a std::future.
- A future is ready if the shared state is available. Or to say it differently if the promise has already produced its value.
Therefore (valid == true) is a requirement for (ready == true).
Whom such as I perceive promise and future as the endpoints of a data channel; I will present my mental picture of validity and readiness. You can see a picture in my post Tasks.
The future is valid if there is a data channel to a promise. The future is ready if the promise has already put value into the data channel.
Now, to the method then.
Continuations with then
then empowers you to attach a future to another future. Here it often happens that a future will be packed into another future. To unwrap the outer future is the job of the unwrapping constructor.
Before I show the first code snippet, I have to say a few words about proposal n3721. Most of this post is from the proposal to “Improvements for std::future<T> and Related APIs”. That also holds for my examples. Strangely, they often did not use the final get call to get the result from the res future. Therefore, I added to the examples the res.get call and saved the result in a variable myResult. Additionally, I fixed a few typos.
There is a subtle difference between the to_string(f.get()) – call (line 7) and the f2.get()-call in line 10: the first call is non-blocking or asynchronous and the second call is blocking or synchronous. The f2.get() – call waits until the result of the future-chain is available. This statement will also hold for chains such as f1.then(…).then(…).then(…).then(…) as it will hold for the composition of extended futures. The final f2.get() call is blocking.
std::async, std::packaged_task, and std::promise
There is not so much to say about the extensions of std::async, std::package_task, and std::promise. I have only to add that all three return in C++20 extended futures.
Therefore, the composition of futures is more exciting. Now we can compose asynchronous tasks.
Creating new futures
C++20 gets four new functions for creating special futures. These functions are std::make_ready_future, std::make_exceptional_future, std::when_all, and std::when_any. At first, to the functions std::make_ready_future, and std::make_exceptional_future.
std::make_ready_future and std::make_exceptional_future
Both functions create an immediately ready future. In the first case, the future has a value; in the second case an exception. What seems to be strange makes a lot of sense. The creation of a ready future requires C++11 a promise. That is even necessary if the shared state is immediately available.
Hence, the result must only be calculated using a promise if (x > 0) holds. A short remark. Both functions are the pendant to the return function in a monad. I have already written about this very interesting aspect of extended futures. My emphasis in this post was more on functional programming in C++20.
Now, let’s finally begin with future composition.
std::when_all und std::when_any
Both functions have a lot in common.
At first, to the input. Both functions accept a pair of iterators to a future range or an arbitrary number of futures. The big difference is that in the case of the pair of iterators, the futures have to be of the same type; that holds not in the case of the arbitrary number of futures they can have different types, and even std::future and std::shared_future can be used.
The function’s output depends if a pair of iterators or an arbitrary number of futures (variadic template) was used. Both functions return a future. If a pair of iterators were used, you would get a future of futures in a std::vector: std::future<std::vector<futureR>>>. If you use use a variadic template, you will get a future of futures in a std::tuple: std::future<std::tuple<future<R0>, future<R1>, … >>.
That was it with their commonalities. The future that both functions return will be ready if all input futures (when_all), or if any of (when_any) the input futures is ready.
The following two examples show the usage of when_all and when_any.
The future all_f (line 9) composes both futures shared_future1 (line 6) and future2 (Zeile 7). The future result in line 11 will be executed if all underlying futures are ready. In this case, the future all_f in line 12 will be executed. The result is available in the future and can be used in line 14.
The future in when_any can be taken by result in line 11. result provides the information which input future is ready. If you don’t use when_any_result, you must ask each future if it is ready. That is tedious.
future_any is the future that will be ready if one of the input futures is ready. future_any.get() in line 11 returns the future result. By using result.futures[result.index] (line 13), you have the ready future, and thanks to ready_future.get(), you can ask for the result of the job.
Latches and barriers support it to synchronize threads via a counter. I will present them in the next post.
Two years later, the future of the futures changed a lot because of executores. Here are the details of executors.