std::future Extensions
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.
Extended futures
std::future has three new methods.
std::future
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.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
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.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <future> using namespace std; int main() { future<int> f1 = async([]() { return 123; }); future<string> f2 = f1.then([](future<int> f) { return to_string(f.get()); // here .get() won’t block }); auto myResult= f2.get(); } |
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.
future<int> compute(int x) { if (x < 0) return make_ready_future<int>(-1); if (x == 0) return make_ready_future<int>(0); future<int> f1 = async([]() { return do_work(x); }); return f1; }
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.
when_all
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <future> using namespace std; int main() { shared_future<int> shared_future1 = async([] { return intResult(125); }); future<string> future2 = async([]() { return stringResult("hi"); }); future<tuple<shared_future<int>, future<string>>> all_f = when_all(shared_future1, future2); future<int> result = all_f.then([](future<tuple<shared_future<int>, future<string>>> f){ return doWork(f.get()); }); auto myResult= result.get(); } |
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.
when_any
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <future> #include <vector> using namespace std; int main(){ vector<future<int>> v{ .... }; auto future_any = when_any(v.begin(), v.end()); when_any_result<vector<future<int>>> result= future_any.get(); future<int>& ready_future = result.futures[result.index]; auto myResult= ready_future.get(); } |
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.
What’s next?
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.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!