-
Notifications
You must be signed in to change notification settings - Fork 3
lazy instead of racy #1
Description
there is a lot of overhead and complexity when letting the value race with the continuation.
just making the contract lazy eliminates this race.
This is not an attempt to be correct or complete, but here is code to show that the race is gone in a lazy future.
#include <iostream>
#include <new>
#include <memory>
#include <thread>
#include <exception>
using namespace std;
#include "../include/futures_static_thread_pool.h"
namespace execution = std::experimental::execution;
using std::experimental::futures_static_thread_pool;
//using executor = std::experimental::futures_static_thread_pool::executor_type;
namespace lzy {
// define types for promise and future
template<class T>
using set_value = function<void(T)>;
template<class E>
using set_error = function<void(E)>;
template<class T, class E = exception_ptr>
using promise = tuple<set_value<T>, set_error<E>>;
template<class T, class E = exception_ptr>
using future = function<void(promise<T, E>)>;
// define operators
// run future on specific executor
template<class T, class E = exception_ptr>
auto on = [] (auto e, future<T, E> source) -> future<T, E> {
return [e, source](promise<T, E> p){
e.execute([source, p](){
source(p);
});
};
};
// run promise on specific executor
template<class T, class E = exception_ptr>
auto via = [] (auto e, future<T, E> source) -> future<T, E> {
return [e, source](promise<T, E> p){
// auto [value, error] = p;
auto value{get<0>(p)};
auto error{get<1>(p)};
source(make_tuple(
[e, value](T t) {e.execute([t, value](){value(t);});},
[e, error](E er) {e.execute([er, error](){error(er);});}
));
};
};
// modify the type and value of T
template<class T, class E = exception_ptr>
auto map = [] (auto f, future<T, E> source) -> future<decltype(f(declval<T>())), E> {
return [f, source](promise<decltype(f(declval<T>())), E> p){
// auto [value, error] = p;
auto value{get<0>(p)};
auto error{get<1>(p)};
source(make_tuple(
[f, value](T t) {value(f(t));},
[error](E er) {error(er);}
));
};
};
// chain another operation
template<class T, class E = exception_ptr>
auto then = [] (auto f, future<T, E> source) {
return [f, source](auto p){
auto error{get<1>(p)};
source(make_tuple(
[f, p](T t) {f(t)(p);},
[error](E er) {error(er);}
));
};
};
// start future and wait for completion
template<class T, class E = exception_ptr>
auto get = [](future<T, E> f){
T result{};
E error{};
condition_variable signal;
mutex lock;
unique_lock<mutex> guard(lock);
f(make_tuple([&](T t){
result = t;
signal.notify_one();
}, [&](E e){
error = e;
signal.notify_one();
}));
signal.wait(guard);
if (!!error){ rethrow_exception(error); }
return result;
};
} // lzy
int main() {
futures_static_thread_pool cpupool{1};
futures_static_thread_pool iopool{1};
auto mywork = [](int i) -> lzy::future<int> {
return [i](lzy::promise<int> p){
auto [value, error] = p; value(i*2);
};
};
// work hasn't started
// easy to enable sugary syntax: mywork(21) | on(iopool.executor()) | via(cpupool.executor());
auto task = lzy::via<int>(cpupool.executor(), lzy::on<int>(iopool.executor(), mywork(21)));
// work hasn't started
// easy to enable sugary syntax: task | then(mywork) | map([](int i){ return to_string(i/2); });
cout << lzy::get<string>(lzy::map<int>([](int i){ return to_string(i/2); }, lzy::then<int>(mywork, task))) << endl;
// work finished - no races.
return 0;
}This also demonstrates that a continuation based future with no executor is the only concept needed to allow get(), on() and via(). I can also implement the racy promise (where work starts immediately, before a promise is attached) by making a new operator similar to get and via. But IMO the guidance should be 'don't do that!'.
adding cancellation will make the value and cancellation race - but that is inherent in cancellation. This code shows that the value racing with then is not inherent to the operation.