std::future Extensions

Contents[Show]

Tasks in the form of promises and futures have in C++11 an ambivalent reputation. At on hand, they are a lot easier to use than threads or condition variables; at the other hand, they have a great deficiency. They can not composed. C++20 will overcome this deficiency.

Before I write about extended futures, let me say a few word 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 what have to be done and not how  - such as for threads - is has to be done. The programmer gives the system some job to perform and the system takes care that the job will be executed by the C++ runtime as smart as possible. That can mean that 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, there is a thread pool that accepts the job and distributes it in a smart way. If that is not an abstraction?

I have written a few posts about tasks in the form of std::async, std::packaged_task, and 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 special futures that are compensable. 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 that unwraps the outer future of a wrapped future (future<future<T>>).
  • The predicate is_ready that returns wheter a shared state is available.
  • The method then that attaches a continuation to a future.

At first, to something quite sophisticated. The state of a 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 me perceive promise and future as the endpoints of a data channel, them I will present my mental picture of valid and ready. 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 its value into the data channel.

Now, to the method then.

Continuations with then

then empowers you to attach a future to another future. Here if often happens that a future will be packed into another future. To unwrap the outer future this is the job of the unwrapping constructor.

Before I show the first code snippet, I have to say a few words about the proposal n3721.  Most of this post is from the proposal to "Improvements for std::future<T> and Releated APIs". That also holds for my examples. Strange, 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 is 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_execptional_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 a future that is immediately ready. 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 futures requires in 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 about the 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 a different types and even std::future and std::shared_future can be used.

The output of the function depends if a pair of iterators or an arbitrary numbers of futures (variadic template) was used. Both functions return a future. If a pair of iterators was used, you will 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 next 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 the 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 in the future result available 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 have to 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 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 synchronise threads via a counter. I will present them in the next post.

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Get your e-book. Support my blog.

Tags: C++20, tasks

Comments   

0 #1 Avis 2017-08-09 10:09
I love it when people get together and share
opinions. Great blog, styick with it!
Quote

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 354

All 538075

Currently are 162 guests and no members online