[future] Early draft of wait for multiple futures interface
We've been discussing the need to implement a mechanism to wait for multiple futures in a separate thread: www.nabble.com/Review-Request%3A-future-library-%28Gaskill-version%29-to16600402.html
I've come up with some interfaces which I find intuitive and useful. I've never implemented thread waiting, so please let me know if you think something like this is viable.
------------------------------ Proposal ------------------------------
A future can be constructed and thereafter wait on four different "future conditions"
* a promise
* a future_switch
* a future_barrier
* another future
Each condition has a specified return type and the futures you create from it must match this type. They all have the same possible states - isn't ready, has value or an expression. I have left these and alot of other details out of the interface below for conciseness.
------------------------------ Interface drafts ------------------------------
/// Future condition which is ready when any of the added conditions is ready. Has an arbitrary return-type
template<class ReturnType>
class future_switch
{
public:
/// Direct return
template<class FutureConditionWithMatchingReturnType>
void add_case(FutureConditionWithMatchingReturnType f);
/// Possibility to perform custom functor, bind state, perform some logic etc before fulfilling it's condition
template<class FutureCondition, class FutureTypeToReturnTypeFunctor>
void add_case(FutureCondition f, FutureTypeToReturnTypeFunctor case);
/// Only valid in is_ready state. Removes last fulfilled futurecondition and enables new futures to be
/// constructed. Existing futures will have copied the value or exception already and will still work.
void remove_ready_case();
bool empty() const;
};
/// Future condition which is ready when all added futures are.
/// This interface is less promising and has some flaws. Maybe you can get inspired by it though.
template<class ReturnType>
class future_barrier
{
public:
future_barrier(const function<ReturnType()>& onReady);
template<class FutureCondition>
void add_condition(const future<FutureType>& f);
};
------------------------------ Example usage ------------------------------
template<class T>
future_switch<T> operator||(const future<T>& lhs, const future<T>& rhs)
{
future_switch<T> sw;
sw.add_case(lhs);
sw.add_case(rhs);
return sw;
}
template<class T>
T and_impl(const future<T>& lhs, const future<T>& rhs)
{
return lhs.get() && rhs.get();
}
template<class T>
future_barrier<T> operator&&(const future<T>& lhs, const future<T>& rhs)
{
// Rather large runtime insecurity, we might forget to add lhs and rhs as conditions to the barrier
future_barrier b(bind(&and_impl, lhs, rhs));
b.add_condition(lhs);
b.add_condition(rhs);
return b;
}
// This is an experimental use case for using switch as a dynamic set of future conditions
// It handles three future responses and calls the three functions on the next lines
bool handle_response_a(ResponseA r);
bool handle_response_b(ResponseB r);
bool handle_response_c(ResponseC r);
void handle_pending_requests(const future<ResponseA>& a,
const future<ResponseB >& b,
const future<ResponseC >& c)
{
future_switch<void> sw;
sw.add_case(a, &handle_repsonse_a);
sw.add_case(b, &handle_repsonse_b);
sw.add_case(c, &handle_repsonse_c);
bool all_ok = true;
while (!sw.empty())
{
// Will automatically remove ok:ed futures from sw
future<bool> next_response_was_ok(sw);
bool all_ok = all_ok && next_response_was_ok.get();
sw.remove_ready_case();
}
return all_ok;
}
------------------------------ Some comments ------------------------------
There is a lot to improve here. I'd appreciate it if we could focus on determining whether this is a viable solution and the way we want to go before diving into details.
These abstractions has the following property:
A Users can compose conditions and poll them without the need of waiting.
B We do not need to spawn extra threads for waiting - we only wait on the "outmost" future
C Threads needn't awake and poll readiness - assuming the and/or logic is handled by waitingcode.
D No "user logic" from the future-blocking threads propagate to the promise-fulfilling thread.
E It's possible to implement efficient laziness by keeping readiness/waiting logic internal in the future library
"wait for all" can be implemented without adding future_barrier, by simply calling wait() on all futures. We lose property A and E for "wait for all" if we do it this way. The blocking thread will awake unnecessarily many times.
Property C means promise-fullfilling threads need to do some basic and/or logic. I think this can be implemented in a safe and efficient manner, O(log(N)) for barrier/all and O(1) for switch/any. I also think it is much preferable to waking and switching to future-blocking threads which re-check readiness conditions. Note property D.
Whatever mechanism we choose I think it's desirable to be able to easily "lift" existing functions to futures;
R foo(A1 a1, A2 a2, A3 a3)
should be easily rewritable as;
future<R> foo(future<A1> a1, A2 a2, future<A3> a3) // Some parameters need not be lifted
Johan