5

I am building a set of futures that work in a GUI thread during event dispatching and want to adopt the API of std::future but have hit an issue with chaining futures (non-blocking asynchronous execution).

Say we have a function that returns a future, and we want to execute something once the future is ready

disconnect().then([](std::future<State> &&f) {
   ...
});

It is my understanding that when doing nothing with the returned future from "then", it will be destroyed and the future will be aborted and the function will not anymore be executed. Therefore to make sure the chain is still present and executed properly, we would have to save the future somewhere (perhaps as a data member).

What should we do if we aren't interested in the returned future, but only in the chain of operations and that the function is execute properly? Should we move the returned future into a new std::future<R>(...) and delete it in the lambda after it finished, such as "delete &f;" ? That looks wrong, though.

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • if the future is optional it should probably be a pointer or a unique_ptr – paulm Dec 11 '15 at 13:51
  • @paulm what do you mean by "if the future is optional"? Does "then" not always return a future whose result becomes the return value of the passed function-object? – Johannes Schaub - litb Dec 11 '15 at 13:52
  • "What should we do if we aren't interested in the returned future" if you dont always want the future then it should be optional – paulm Dec 11 '15 at 13:53
  • If "then" does not return a future, then how will the state of the computation be stored? Let's assume for the purpose of this question, that we're working on std::future of the concurrency TS. How would I trigger such a chain without storing the futures? – Johannes Schaub - litb Dec 11 '15 at 13:54
  • "It is my understanding that when doing nothing with the returned future from "then", it will be destroyed and the future will be aborted and the function will not anymore be executed. " Where in the concurrency TS does it say that? – T.C. Dec 11 '15 at 21:13
  • @T.C. I read that on http://en.cppreference.com/w/cpp/thread/future/~future . Not sure whether "then" has other behavior like async has? – Johannes Schaub - litb Dec 11 '15 at 21:16
  • Unless you are talking about `async` with `deferred`, I don't see how that wording changes anything. Presumably whatever asynchronous provider created by `then` will hold a reference to the shared state. – T.C. Dec 11 '15 at 21:20
  • @T.C. ah so when calling .then and not storing the result, it will not block or simply cancel the execution? I wonder whether this is guaranteed? – Johannes Schaub - litb Dec 11 '15 at 21:23
  • C++14 specified that `~future` may _only_ block if it was created by a call to `async`, is not yet ready, and this is the last reference to that shared state. (30.6.4/5.3 in N4296) (I just updated the cppreference.com link above to clarify this). C++11 implied this because the the `thread` object created by `async` is stored in the shared state, but also left the field open for `~future` to block any other time it felt like it. – TBBle Jan 15 '16 at 05:49
  • C++11 actually required this behaviour explicitly for `async` (30.6.8/5 in N3337 and N4296). – TBBle Jan 15 '16 at 05:56

1 Answers1

2

I don't believe that you need to hold the future to ensure your completion handler is called. Destroying the second future doesn't require that the shared_state is destroyed, it just decreases its reference count.

One way of implementing then by yourself, from Herb Sutter's C++ Concurrency presentation in 2012, is as a free function that calls std::async (page 6 of the slides PDF):

template<typename Fut, typename Work>
auto then( Fut f, Work w ) -> future<decltype(w(f.get()))>
  { return async([=]{ w( f.get() ); }); }

This occupies a thread, but you get the semantics that the caller of then( disconnect(), [](std::future<State> &&f) {...}); doesn't need to block. And even if we throw away the result of that then call, the returned future is destroyed, and its shared state cleaned up, but future f that you passed in is not destroyed.

Then there's the rule that destroying a future returned by async may block until the async completes, which means that if you throw away the result of this then implementation, you may block anyway (page 21). So you can't do that after all.

In the desired effect, then is going to operate as if it put a reference to your function object into a 'continuation slot' of the shared state of the future, along with a second promise of the return type of your function object, and returns the future of that second promise. So the original future's shared state is referenced by the original promise, and the second future's shared state is referenced by the second promise, so neither shared state will be destroyed by whatever you do with the second future.

Once the original promise is fulfilled, the shared state sees it has the continuation slot occupied, so does a make_ready_future out of the value just fulfilled and gives it to your function object, setting the value into the second promise, which has no effect because you threw the second future away. But the "no effect" is on the output of your lambda.

(Implementation-wise, the second future may well be the only reference to the second shared state, the promise in there is just an illustration of behaviour, that the second shared state can be destroyed without forcing the first shared state to be destroyed. And I expect internally it can do better than make_ready_future on the value...)

Community
  • 1
  • 1
TBBle
  • 1,436
  • 10
  • 27
  • Those are very interesting insights into how `then()` can work even when the `future` on which it was called is destructed. So, is it guaranteed that it is always safe to call `then()` on an expiring future? Expiring because it is just a temporary as in the example, or because it will go out of scope before the completion handler is invoked. (I wish [en.cppreference.com](http://en.cppreference.com/w/cpp/experimental/future/then) had a statement regarding relative lifetime expectations). – Ad N Sep 06 '17 at 09:16
  • Yes, I believe this is guaranteed. `then()` interacts with the "old" shared state of the `this` future: the "old" shared state either has a `promise`, `async` or other 'asynchronous task' still keeping it alive; or it contains a value or exception which will be applied to the "new" shared state, at which point the old shared state is no longer interesting and will disappear. *Either* way, the `this` future is no longer interesting, and no longer `valid()`. – TBBle Sep 06 '17 at 12:42
  • As a side-note, I believe that [stlab::future](http://stlab.cc/libraries/concurrency/future/future/) implements the behaviour this question was expecting, and allow a packaged task with no remaining output `future` to itself not run, rolling the task-chain back up to the last place where someone might observe the output. This version of `future` is also closer to `shared_future` in semantics, so `then` doesn't invalidate the `future` object. That means you must hold on to intermediate `future`s you don't want to abort the whole chain, but also that you *can* hold onto them. – TBBle Sep 06 '17 at 12:48