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 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 as smart as possible by the c++ runtime. 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 of 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 produces its value.

Therefore (valid == true) is a requirement for (ready==true).

Whom such as me perceives 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 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 that 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.  The most of this post is from the proposal to "Improvements for std::future<T> and Releated APIs". That includes my examples too. 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.  As I already mentioned it in the code snippet: 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_exception_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 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 for 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 in the case of the arbitrary number of futures can have different type 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: : futur<vector<futureR>>>.  If you use use a variadic template, you will get a futures of futures in a std::tuple: future<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) 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 if 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 some 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 499

All 460328

Currently are 172 guests and no members online