Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concurrency TS: std::future<...>::then, how to keep the chain alive without storing the returned future?

Tags:

c++

c++17

future

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.

like image 750
Johannes Schaub - litb Avatar asked Oct 31 '22 13:10

Johannes Schaub - litb


1 Answers

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...)

like image 157
TBBle Avatar answered Nov 13 '22 15:11

TBBle