allocation aware futures asynchronous ... - meeting c++meeting c++ 2018. 2 introduction about me...

89
Continuable asynchronous programming with allocation aware futures /Naios/continuable Denis Blank <[email protected]> Meeting C++ 2018

Upload: others

Post on 13-Jul-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Continuableasynchronous programming with

allocation aware futures

/Naios/continuable

Denis Blank <[email protected]>

Meeting C++ 2018

Page 2: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

2

IntroductionAbout me

Denis Blank

● Master’s student @Technical University of Munich

● GSoC participant in 2017 @STEllAR-GROUP/hpx

● Author of the continuable and function2 libraries

● Interested in: compiler engineering, asynchronous

programming and metaprogramming

Page 3: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

3

IntroductionTable of contents

1. The future pattern (and its disadvantages)

2. Rethinking futures○ Continuable implementation

○ Usage examples of continuable

3. Connections○ Traversals for arbitrarily nested packs

○ Expressing connections with continuable

4. Coroutines

The continuable library talk:

/Naios/continuable

Page 4: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

The future pattern

4

Page 5: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

The future pattern

5

promises and futures

Future result

std::future<int>

Resolver

std::promise<int>

Creates

Resolves

Page 6: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

std::promise<int> promise;

std::future<int> future = promise.get_future();

promise.set_value(42);

int result = future.get();

The future pattern

6

In C++17 we can only pool or wait for the result synchronously

Synchronous wait

Page 7: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

future<std::string> other = future

.then([](future<int> future) {

return std::to_string(future.get());

});

Resolve the next future

Asynchronous return types

The future pattern

The Concurrency TS proposed a then method for

adding a continuation handler, now reworked in the

“A Unified Futures” and executors proposal.

7

Asynchronous continuation chaining

Page 8: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

The future pattern

8

The shared state

std::promise<int>std::future<int>

Shared stateon the heap

Page 9: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

The future pattern

9

template<typename T>

class shared_state {

std::variant<

std::monostate, T, std::exception_ptr

> result_;

std::function<void(future<T>)> then_;

std::mutex lock_;

};

Shared state implementation

Simplified version

The shared state contains a result storage,

continuation storage and synchronization primitives.

Page 10: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

The future pattern

10

● std::future

● boost::future

● folly::Future

● hpx::future

● stlab::future

● ...

Implementations with a shared state

Page 11: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

11

Future disadvantagesShared state overhead

● Attaching a continuation (then) creates a new future

and shared state every time (allocation overhead)!

● Maybe allocation for the continuation as well

● Result read/write not wait free○ Lock acquisition or spinlock

○ Can be optimized to an atomic wait free state read/write in the

single producer and consumer case (non shared future/promise).

● If futures are shared across multiple cores:

Shared-nothing futures can be zero cost (Seastar).

Page 12: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

12

Future disadvantagesShared state overhead

● Attaching a continuation (then) creates a new future

and shared state every time (allocation overhead)!

● Maybe allocation for the continuation as well

● Result read/write not wait free○ Lock acquisition or spinlock

○ Can be optimized to an atomic wait free state read/write in the

single producer and consumer case (non shared future/promise).

● If futures are shared across multiple cores:

Shared-nothing futures can be zero cost (Seastar).

Page 13: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

13

Future disadvantagesStrict eager evaluation

std::future<std::string> future = std::async([] {

return "Hello Meeting C++!"s;

});

● Futures represent the asynchronous result of an

already running operation!

● Impossible not to request it

● Execution is non deterministic:○ Leads to unintended side effects!

○ No ensured execution order!

● Possible: Wrapping into a lambda to achieve laziness.

Page 14: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

14

Future disadvantagesStrict eager evaluation

● Futures represent the asynchronous result of an

already running operation!

● Impossible not to request it

● Execution is non deterministic:○ Leads to unintended side effects!

○ No ensured execution order!

● Possible: Wrapping into a lambda to achieve laziness.

std::future<std::string> future = std::async([] {

return "Hello Meeting C++!"s;

});

Page 15: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

15

Future disadvantagesUnwrapping and R-value correctness

● future::then L-value callable although consuming ○ Should be R-value callable only (for detecting misuse)

● Always required to call future::get○ But: Fine grained exception control possible (not needed)

● Repetition of type○ Becomes worse in compound futures (connections)

future.then([] (future<std::tuple<future<int>,

future<int>>> future) {

int a = std::get<0>(future.get()).get();

int b = std::get<1>(future.get()).get();

return a + b;

});

Page 16: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

16

Future disadvantagesUnwrapping and R-value correctness

● future::then L-value callable although consuming ○ Should be R-value callable only (for detecting misuse)

● Always required to call future::get○ But: Fine grained exception control possible (not needed)

● Repetition of type○ Becomes worse in compound futures (connections)

future.then([] (future<std::tuple<future<int>,

future<int>>> future) {

int a = std::get<0>(future.get()).get();

int b = std::get<1>(future.get()).get();

return a + b;

});

Page 17: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

17

Future disadvantagesException propagation

● Propagation overhead through rethrowing on get

● No error codes as exception type possible

make_exceptional_future<int>(std::exception{})

.then([] (future<int> future) {

int result = future.get();

return result;

})

.then([] (future<int> future) {

int result = future.get();

return result;

})

.then([] (future<int> future) {

try {

int result = future.get();

} catch (std::exception const& e) {

// Handle the exception

}

});

Page 18: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

18

Future disadvantagesAvailability

● std::future::experimental::then will change heavily:○ Standardization date unknown○ “A Unified Future” proposal maybe C++23

● Other implementations require a large framework, runtime or are difficult to build

Now C++20 C++23 Future

Page 19: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Rethinking futures

19

Page 20: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

20

Rethinking futures

● Usable in a broad case of usage scenarios (boost, Qt)

● Portable, platform independent and simple to use

● Agnostic to user provided executors and runtimes

● Should resolve the previously mentioned disadvantages:

○ Shared state overhead

○ Strict eager evaluation

○ Unwrapping and R-value correctness

○ Exception propagation

○ Availability

Designing goals

Page 21: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

21

Rethinking futures

● Usable in a broad case of usage scenarios (boost, Qt)

● Portable, platform independent and simple to use

● Agnostic to user provided executors and runtimes

● Should resolve the previously mentioned disadvantages:

○ Shared state overhead

○ Strict eager evaluation

○ Unwrapping and R-value correctness

○ Exception propagation

○ Availability

Designing goals

Page 22: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

22

Rethinking futuresWhy we don’t use callbacks

● Difficult to express complicated chains

● But: Simple and performant to express an

asynchronous continuation.

● But: Work nicely with existing libraries

signal_set.async_wait([](auto error, int slot) {

signal_set.async_wait([](auto error, int slot) {

signal_set.async_wait([](auto error, int slot) {

signal_set.async_wait([](auto error, int slot) {

// handle the result here

});

});

});

});

Callback hell

Page 23: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

23

Rethinking futuresHow we could use callbacks

● Idea: Transform the callbacks into something

easier to use without the callback hell○ Long history in JavaScript: q, bluebird

○ Much more complicated in C++ because of static typing,

requires heavy metaprogramming.

● Mix this with syntactic sugar and C++ candies

like operator overloading.

And finished is the continuable

Not trivial...

Page 24: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

24

Rethinking futuresHow we could use callbacks

● Idea: Transform the callbacks into something

easier to use without the callback hell○ Long history in JavaScript: q, bluebird

○ Much more complicated in C++ because of static typing,

requires heavy metaprogramming.

● Mix this with syntactic sugar and C++ candies

like operator overloading.

And finished is the continuable

Not trivial...

Page 25: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

auto continuable = make_continuable<int>([](auto&& promise) {

// Resolve the promise immediately or store

// it for later resolution.

promise.set_value(42);

});

Resolve the promise,set_value alias for operator()

Arbitrary asynchronous return types

Creating continuables

A continuable_base is creatable through

make_continuable, which requires its types trough

template arguments and accepts a callable type

The promise might be moved or stored

25

Page 26: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

auto continuable = make_continuable<int>([](auto&& promise) {

// Resolve the promise immediately or store

// it for later resolution.

promise.set_value(42);

});

Resolve the promise,set_value alias for operator()

Arbitrary asynchronous return types

Creating continuables

A continuable_base is creatable through

make_continuable, which requires its types trough

template arguments and accepts a callable type

The promise might be moved or stored

26

Page 27: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

make_ready_continuable(42)

.then([] (int value) {

// return something

});

Chaining continuables

A continuable_base is chainable through its then

method, which accepts a continuation handler.

We work on values directly rather than continuables.

This ready continuable resolves the given result instantly

27

Continuation chaining

Optional return value:● Plain object● Tuple of objects● The next continuable

to resolve

Page 28: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

http_request("example.com")

.then([] (int status, std::string body) {

return mysql_query("SELECT * FROM `users` LIMIT 1");

})

.then(do_delete_caches())

.then(do_shutdown());

Chaining continuables

then may also return plain objects, a tuple of

objects or the next continuable_base to resolve.

Return the next continuable_base

to resolve

Just a dummy function which returns a continuable_base of int, std::string

28

Continue from callbacks

Ignore previous results

Page 29: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

http_request("example.com")

.then([] (int status, std::string body) {

return mysql_query("SELECT * FROM `users` LIMIT 1");

})

.then(do_delete_caches())

.then(do_shutdown());

Chaining continuables

then may also return plain objects, a tuple of

objects or the next continuable_base to resolve.

Return the next continuable_base

to resolve

Just a dummy function which returns a continuable_base of int, std::string

29

Continue from callbacks

Ignore previous results

Page 30: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

make_ready_continuable(‘a’, 2, 3)

.then([] (char a) {

return std::make_tuple(‘d’, 5);

})

.then([] (char c, int d) {

// ...

});

Chaining continuables

The continuation passed to then may also accept the

result partially, and may pass multiple objects

wrapped inside a std::tuple to the next handler.

Return multiple objects that are passed to the

next continuation directly

Use the asynchronous arguments partially

30

Continuation chaining sugar

Page 31: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

make_ready_continuable(‘a’, 2, 3)

.then([] (char a) {

return std::make_tuple(‘d’, 5);

})

.then([] (char c, int d) {

// ...

});

Chaining continuables

The continuation passed to then may also accept the

result partially, and may pass multiple objects

wrapped inside a std::tuple to the next handler.

Use the asynchronous arguments partially

31

Continuation chaining sugar

Return multiple objects that are passed to the

next continuation directly

Page 32: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Continuable implementation

32

Page 33: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

33

Continuable implementationCreating ready continuables

make_ready_continuable(0, 1)

make_continuable<int, int>([] (auto&& promise) {

promise.set_value(0, 1);

});

The implementation stores the arguments into a

std::tuple first and sets the promise with the

content of the tuple upon request (std::apply).

Page 34: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

34

Continuable implementationDecorating the continuation result

.then([] (auto result) {

return;

})

.then([] (auto result) {

return make_ready_continuable();

})

Transform the continuation result such that it is

always a continuable_base of the corresponding

result.

.then([] (auto result) {

return std::make_tuple(0, 1);

})

.then([] (auto result) {

return make_ready_continuable(0, 1);

})

Page 35: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

35

Continuable implementationDecorating the continuation result

.then([] (auto result) {

return;

})

.then([] (auto result) {

return make_ready_continuable();

})

Transform the continuation result such that it is

always a continuable_base of the corresponding

result.

.then([] (auto result) {

return std::make_tuple(0, 1);

})

.then([] (auto result) {

return make_ready_continuable(0, 1);

})

Page 36: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

36

Continuable implementationInvoker selection through tag dispatching

using result_t = std::invoke_result_t<Callback, Args...>;

// ^ std::tuple<int, int> for example

auto invoker = invoker_of(identity<result_t>{});

// void

auto invoker_of(identity<void>);

// T

template<typename T>

auto invoker_of(identity<T>);

// std::tuple<T...>

template<typename... T>

auto invoker_of(identity<std::tuple<T...>>);

3

2

1

Page 37: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

37

Continuable implementationAttaching a continuation

auto continuation =

[=](auto promise) {

promise(1);

};

auto callback =

[] (int result) {

return make_ready_continuable();

};

Attaching a callback to a continuation yields a new

continuation with new argument types.

auto new_continuation =

[](auto next_callback) {

auto proxy = decorate(callback,

next_callback)

continuation(proxy);

};

Page 38: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

38

Continuable implementationDecorating the callback

auto proxy = [ callback,

next_callback ] (auto&&... args) {

auto next_continuation = callback(std::forward<decltype(args)>(args)...);

next_continuation(next_callback);

};

The proxy callback passed to the previous

continuation invokes the next continuation with the

next callback.

Page 39: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

39

Continuable implementationSeeing the big picture

Yield result ofouter continuation

continuationcallback

continuationcallback

continuationcallback

continuationcallbackcontinuationcallback

Invocation

Page 40: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

40

Continuable implementationSeeing the big picture

continuationcallback

continuationcallback

continuationcallback

continuationcallbackcontinuationcallback

Invocation

Yield result ofouter continuation

callback passed to this continuation

through then!

Page 41: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Russian Matryoshka doll

41

Continuable implementationSeeing the big picture

Page 42: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

42

Continuable implementationException handling

When the promise is resolved with an exception an

exception_ptr is passed to the next available

failure handler.

read_file("entries.csv")

.then([] (std::string content) {

// ...

})

.fail([] (std::exception_ptr exception) {

// handle the exception

})

promise.set_exception(...)

On exceptions skip the result handlers between.

Page 43: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

43

Continuable implementationSplit asynchronous control flows

Results

Exceptions

Others

throw recover

Page 44: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

44

Continuable implementationSplit asynchronous control flows

template<typename... Args>

struct callback {

auto operator() (Args&&... args);

auto operator() (dispatch_error_tag, std::exception_ptr);

// dispatch_error_tag is exception_arg_t in the

// "Unified Futures" standard proposal.

};

Or any other error type

Page 45: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

45

Continuable implementationException propagation

template<typename... Args>

struct proxy {

Callback failure_callback_;

NextCallback next_callback_

void operator() (Args&&... args) {

// The next callback has the same signature

next_callback_(std::forward<Args>(args)...);

}

void operator() (dispatch_error_tag, std::exception_ptr exception) {

failure_callback_(exception);

}

};

On a valid result forward it to the next available result handler

Page 46: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

46

Continuable implementationResult handler conversion

template<typename... Args>

struct proxy {

Callback callback_;

NextCallback next_callback_

void operator() (Args&&... args) {

auto continuation = callback_(std::forward<Args>(args)...);

continuation(next_callback);

}

void operator() (dispatch_error_tag, std::exception_ptr exception) {

next_callback_(dispatch_error_tag{}, exception);

}

};

Forward the exception to the next available handler

Page 47: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

47

The continuable_baseThe wrapper

The continuable_base is convertible when the types

of Continuation are convertible to each other.

template<typename Continuation, typename Strategy>

class continuable_base {

Continuation continuation_;

ownership ownership_;

template<typename C,

typename E = this_thread_executor>

auto then(C&& callback,

E&& executor = this_thread_executor{}) &&;

};

● void (until now)● strategy_all_tag● strategy_seq_tag● strategy_any_tag

consuming = R-valuestd::move(continuable).then(...);

Page 48: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

48

The continuable_baseThe wrapper

The continuable_base is convertible when the types

of Continuation are convertible to each other.

template<typename Continuation, typename Strategy>

class continuable_base {

Continuation continuation_;

ownership ownership_;

template<typename C,

typename E = this_thread_executor>

auto then(C&& callback,

E&& executor = this_thread_executor{}) &&;

};

● void (until now)● strategy_all_tag● strategy_seq_tag● strategy_any_tag

consuming = R-valuestd::move(continuable).then(...);

Page 49: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

49

The continuable_baseThe ownership model

The continuation is invoked when the

continuable_base is still valid and being destroyed

(race condition free continuation chaining).

npc->talk("Greetings traveller, how is your name?")

.then([log, player] {

log->info("Player {} asked for name.", player->name());

return player->ask_for_name();

})

.then([](std::string name) {

// ...

});Invoke the

continuation here

Page 50: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

50

Memory allocationThe continuable_base

● Increases the amount of types the compiler has to generate ⇒ slower compilation

● Better for compiler optimization● Increases the executable size● ⇒ We require a concrete type for APIs where we

don’t want to expose our implementation

Until now: no memory allocation involved!then always returns an object of an unknown type

Page 51: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

51

Concrete typesThe continuable_base

continuable<int, std::string> http_request(std::string url) {

return [=](promise<int, std::string> promise) {

// Resolve the promise later

promise.set_value(200, "<html> ... </html>");

};

}

Preserve unknown types across the continuation chaining, convert it to concrete types in APIs on request

Page 52: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

52

The continuable_baseType erasure

For the callable type erasure my function2 library

is used that provides move only and multi signature

capable type erasures + small functor optimization.

using callback_t = function<void(Args...),

void(dispatch_error_tag,

std::exception_ptr)>;

using continuation_t = function<void(callback_t)>;

Erased callable for promise<Args…>

Erased callable for continuable<Args…>

Page 53: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

53

The continuable_baseType erasure

For the callable type erasure my function2 library

is used that provides move only and multi signature

capable type erasures + small functor optimization.

using callback_t = function<void(Args...),

void(dispatch_error_tag,

std::exception_ptr)>;

using continuation_t = function<void(callback_t)>;

Erased callable for promise<Args…>

Erased callable for continuable<Args…>

Page 54: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

54

The continuable_baseType erasure aliases

template<typename... Args>

using promise = promise_base<callback_t<Args...>>;

template<typename... Args>

using continuable = continuable_base<

function<void(promise<Args...>)>,

void

>;

template<typename... Args>

using callback_t = function<void(Args...),

void(dispatch_error_tag,

std::exception_ptr)>;

continuable_base type erasure works implicitly and

with any type erasure wrapper out of the box.

Page 55: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

55

The continuable_baseApply type erasure when needed

futures requires a minimum of two fixed allocations

per then whereas continuable requires a maximum of

two allocations per type erasure.

// auto do_sth();

continuable<> cont = do_sth()

.then([] {

return do_sth();

})

.then([] {

return do_sth();

});

// future<void> do_sth();

future<void> cont = do_sth()

.then([] (future<void>) {

return do_sth();

})

.then([] (future<void>) {

return do_sth();

});

Max 2 allocs on need

2*2 fixed + 2*1 maybe continuationsallocs

Page 56: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

56

The continuable_baseApply type erasure when needed

futures requires a minimum of two fixed allocations

per then whereas continuable requires a maximum of

two allocations per type erasure.

// auto do_sth();

continuable<> cont = do_sth()

.then([] {

return do_sth();

})

.then([] {

return do_sth();

});

// future<void> do_sth();

future<void> cont = do_sth()

.then([] (future<void>) {

return do_sth();

})

.then([] (future<void>) {

return do_sth();

});

Max 2 allocs on need

2*2 fixed + 2*1 maybe continuationsallocs

Page 57: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Executor support

57

Page 58: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

58

Usage casesExecutor support

mysql_query("SELECT `id`, `name` FROM `users` WHERE `id` = 123")

.then([](ResultSet result) {

// On which thread this continuation runs?

});

promise.set_value(result);

● On which thread the continuation runs:○ Resolving thread? (default)○ Thread which created the continuation?

● When does the continuation run:○ Immediately on resolving? (default)○ Later?

● Can we cancel the continuation chain?

That should be up to you!

Page 59: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

59

Using an executorExecutor support

struct my_executor_proxy {

template<typename T>

void operator()(T&& work) {

std::forward<T>(work)();

}

};

mysql_query("SELECT `id`, `name` FROM `users` WHERE `id` = 123")

.then([](ResultSet result) {

// Pass this continuation to my_executor

}, my_executor_proxy{});

second argument of then

● Invoke the work● Drop the work● Move the work to another

thread or executor

Page 60: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

60

No executor propagationExecutor support

The executor isn’t propagated to the next handler

and has to be passed again to avoid unnecessary

type erasure (we could make it a type parameter).

continuable<> next = do_sth().then([] {

// Do sth.

}, my_executor_proxy{});

std::move(next).then([] {

// No ensured propagation!

});

Propagation would lead to type erasure although it isn’t requested here!

Page 61: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Context of executionExecutor support

⇒ We can neglect executor propagation when moving heavy tasks to a continuation,

except in case of data races!

do_sth().then([] {

// Do something short

});

continuable<> do_sth() {

return [] (auto&& promise) {

// Do something long

promise.set_value();

};

}

ContinuationCallback

Page 62: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

62

Check design goals

● No shared state overhead

● No strict eager evaluation

● No unwrapping and R-value correctness

● Exception propagation

● Availability

○ C++14

○ Header-only (depends on function2)

○ GCC / Clang / MSVC

Page 63: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

63

Check design goals

● No Shared state overhead

● No strict eager evaluation

● No Unwrapping and R-value correctness

● Exception propagation

● Availability

○ C++14

○ Header-only (depends on function2)

○ GCC / Clang / MSVC

Page 64: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Connections

64

Page 65: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

65

ConnectionsThe call graph

when_all, when_any usable to express relations

between multiple continuables.

⇒ Guided/graph based execution requires a shared

state (not available)

when_all

then

when_any

Ready when any dependent continuable is ready

Ready when all dependent continuables are ready

Page 66: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

66

ConnectionsLazy evaluation advantages

Using lazy (on request) evaluation over an eager one

makes it possible to choose the evaluation strategy.

⇒ Moves this responsibility from the executor to

the evaluator!

when_seq

Invokes all dependent continuables in sequential order

Thoughts(not implemented)

● when_pooled - pooling● when_every - request all● when_first_succeeded /

when_first_failed - exception strategies

Page 67: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

67

ConnectionsSimplification over std::experimental::when_all

std::when_all introduces code overhead because of

unnecessary fine grained exception handling.

// continuable<int> do_sth();

when_all(do_sth(),

do_sth())

.then([] (int a, int b) {

return a == b;

});

// std::future<int> do_sth();

std::experimental::when_all(do_sth(),

do_sth())

.then([] (std::tuple<std::future<int>,

std::future<int>> res) {

return std::get<0>(res).get()

== std::get<1>(res).get();

});

Page 68: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

68

ConnectionsSimplification over std::experimental::when_all

std::when_all introduces code overhead because of

unnecessary fine grained exception handling.

// continuable<int> do_sth();

when_all(do_sth(),

do_sth())

.then([] (int a, int b) {

return a == b;

});

// std::future<int> do_sth();

std::experimental::when_all(do_sth(),

do_sth())

.then([] (std::tuple<std::future<int>,

std::future<int>> res) {

return std::get<0>(res).get()

== std::get<1>(res).get();

});

Page 69: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

69

Connections implementationBased on GSoC @STEllAR-GROUP/hpx

The map_pack, traverse_pack and traverse_pack_async

API helps to apply an arbitrary connection between

continuables contained in a variadic pack.

hpx::when_all

hpx::dataflowhpx::unwrapped

hpx::when_any

hpx::when_somehpx::wait_any

hpx::wait_some

hpx::wait_all

synchronous traversal (map_pack)

synchronous mapping (traverse_pack)

asynchronous traversal(traverse_pack_async)

Basically the same implementation

Page 70: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

70

Connections implementationBased on GSoC @STEllAR-GROUP/hpx

The map_pack, traverse_pack and traverse_pack_async

API helps to apply an arbitrary connection between

continuables contained in a variadic pack.

hpx::when_all

hpx::dataflowhpx::unwrapped

hpx::when_any

hpx::when_somehpx::wait_any

hpx::wait_some

hpx::wait_all

synchronous traversal (map_pack)

synchronous mapping (traverse_pack)

asynchronous traversal(traverse_pack_async)

Basically the same implementation

Page 71: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

71

Connections implementationIndexer example (map_pack)

Because when_any returns the first ready result of

a common denominator, map_pack could be used to

apply an index to the continuables.

index_continuables(do_sth(),

do_sth(),

do_sth());

// Shall return:

std::tuple<

continuable<size_t /*= 0*/, int>,

continuable<size_t /*= 1*/, int>,

continuable<size_t /*= 2*/, int>

>

// continuable<int> do_sth();

when_any(do_sth(), do_sth(),

do_sth());

.then([] (int a) {

// ?: We don’t know which

// continuable became ready

});

Page 72: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

72

Connections implementationIndexer example (map_pack)

Because when_any returns the first ready result of

a common denominator, map_pack could be used to

apply an index to the continuables.

index_continuables(do_sth(),

do_sth(),

do_sth());

// Shall return:

std::tuple<

continuable<size_t /*= 0*/, int>,

continuable<size_t /*= 1*/, int>,

continuable<size_t /*= 2*/, int>

>

// continuable<int> do_sth();

when_any(do_sth(), do_sth(),

do_sth());

.then([] (int a) {

// ?: We don’t know which

// continuable became ready

});

Page 73: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

73

Connections implementationIndexer example (map_pack)

map_pack(indexer{}, do_sth(), do_sth(), do_sth());

struct indexer {

size_t index = 0;

template <typename T,

std::enable_if_t<is_continuable<std::decay_t<T>>::value>* = nullptr>

auto operator()(T&& continuable) {

auto current = ++index;

return std::forward<T>(continuable).then([=] (auto&&... args) {

return std::make_tuple(current,

std::forward<decltype(args)>(args)...);

});

}

};

map_pack transforms an arbitrary argument pack

through a callable mapper.

Page 74: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

74

Connections implementationIndexer example (map_pack)

map_pack(indexer{}, do_sth(), do_sth(), do_sth());

struct indexer {

size_t index = 0;

template <typename T,

std::enable_if_t<is_continuable<std::decay_t<T>>::value>* = nullptr>

auto operator()(T&& continuable) {

auto current = ++index;

return std::forward<T>(continuable).then([=] (auto&&... args) {

return std::make_tuple(current,

std::forward<decltype(args)>(args)...);

});

}

};

map_pack transforms an arbitrary argument pack

through a callable mapper.

Page 75: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

75

Connections implementationArbitrary and nested arguments

map_pack and friends can work with plain values and

nested packs too and so can when_all.

continuable<int> aggregate(std::tuple<int,

continuable<int>,

std::vector<continuable<int>>> all) {

return when_all(std::move(all))

.then([] (std::tuple<int, int, std::vector<int>> result) {

int aggregated = 0;

traverse_pack([&] (int current) {

aggregated += current;

}, std::move(result));

return aggregated;

});

}

Page 76: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

76

Connections implementationArbitrary and nested arguments

map_pack and friends can work with plain values and

nested packs too and so can when_all.

continuable<int> aggregate(std::tuple<int,

continuable<int>,

std::vector<continuable<int>>> all) {

return when_all(std::move(all))

.then([] (std::tuple<int, int, std::vector<int>> result) {

int aggregated = 0;

traverse_pack([&] (int current) {

aggregated += current;

}, std::move(result));

return aggregated;

});

}

Page 77: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

77

Connections implementationwhen_all/when_seq

Connections require a shared state by design,

concurrent writes to the same box never happen.

int,continuable<int>, std::vector<continuable<int>>

when_all/seq

map_pack(boxify{}, ...)

Ready

traverse_pack(resolve{}, ...)

Continue

map_pack(unwrap{}, ...)

Counter

int,box<expected<int>, continuable<int>>, std::vector<box<expected<int>, continuable<int>>>

Page 78: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

78

Connections implementationwhen_all/when_seq

Connections require a shared state by design,

concurrent writes to the same box never happen.

int,continuable<int>, std::vector<continuable<int>>

int,box<expected<int>, continuable<int>>, std::vector<box<expected<int>, continuable<int>>>

when_all/seq

map_pack(boxify{}, ...)

Ready

traverse_pack(resolve{}, ...)

Continue

map_pack(unwrap{}, ...)

Counter

Page 79: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

79

Connections implementationwhen_all/when_seq

Connections require a shared state by design,

concurrent writes to the same box never happen.

int,continuable<int>, std::vector<continuable<int>>

when_all/seq

map_pack(boxify{}, ...)

Ready

traverse_pack(resolve{}, ...)

Continue

map_pack(unwrap{}, ...)

Counter

int,box<expected<int>, continuable<int>>, std::vector<box<expected<int>, continuable<int>>>

Page 80: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

80

Operator overloadingExpress connections

Operator overloading allows expressive connections

between continuables.

(http_request("example.com/a") && http_request("example.com/b"))

.then([] (http_response a, http_response b) {

// ...

return wait_until(20s)

|| wait_key_pressed(KEY_SPACE)

|| wait_key_pressed(KEY_ENTER);

});

when_any: operator||

when_all: operator&&

Page 81: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

81

Operator overloadingDifficulties

A naive operator overloading approach where we

instantly connect 2 continuables would lead to

unintended evaluations and thus requires linearization.

return wait_until(20s)

|| wait_key_pressed(KEY_SPACE)

|| wait_key_pressed(KEY_ENTER); when_any

when_any

wait_key_pressed(KEY_SPACE)

wait_until(20s)

wait_key_pressed(KEY_ENTER)

Page 82: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

82

Operator overloadingCorrect operator evaluation required

when_any

wait_key_pressed(KEY_SPACE)

wait_until(20s) wait_key_pressed(KEY_ENTER)

Page 83: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

83

Operator overloadingImplementation

Set the continuable_base into an intermediate state

(strategy), materialize the connection on use or when

the strategy changes (expression template).

wait_until(20s)|| wait_key_pressed(KEY_SPACE)

... || wait_key_pressed(KEY_SPACE)

.then(...)

continuable_base<std::tuple<..., ...>, strategy_any_tag>

continuable_base<std::tuple<..., ..., ...>, strategy_any_tag>

continuable_base<..., void>Materialization

Page 84: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

Continuable & Coroutines TS

84

Page 85: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

85

Continuable & Coroutines TSInteroperability

continuable_base implements operator co_await() and

specializes coroutine_traits and thus is compatible to

the Coroutines TS.

continuable<int> interoperability_check() {

try {

auto response = co_await http_request("example.com/c");

} catch (std::exception const& e) {

co_return 0;

}

auto other = cti::make_ready_continuable(0, 1);

auto [ first, second ] = co_await std::move(other);

co_return first + second;

}

Page 86: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

86

Continuable & Coroutines TSInteroperability

continuable_base implements operator co_await() and

specializes coroutine_traits and thus is compatible to

the Coroutines TS.

continuable<int> interoperability_check() {

try {

auto response = co_await http_request("example.com/c");

} catch (std::exception const& e) {

co_return 0;

}

auto other = cti::make_ready_continuable(0, 1);

auto [ first, second ] = co_await std::move(other);

co_return first + second;

}

Page 87: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

87

Continuable & Coroutines TSDo Coroutines deprecate Continuables?

● A coroutine isn’t necessarily allocation free○ Recursive coroutines frames○ Depends on compiler optimization

● Connections

● Executors (difficult to do with plain coroutines)

● Takes time until Coroutines are widely supported○ Libraries that work with plain callbacks (legacy codebases)

● But: Coroutines have much better Call Stacks!

Probably not!There are many things a plain coroutine doesn’t provide

Page 88: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

88

Continuable & Coroutines TSDo Coroutines deprecate Continuables?

● A coroutine isn’t necessarily allocation free○ Recursive coroutines frames○ Depends on compiler optimization

● Connections

● Executors (difficult to do with plain coroutines)

● Takes time until Coroutines are widely supported○ Libraries that work with plain callbacks (legacy codebases)

● But: Coroutines have much better Call Stacks!

Probably not!There are many things a plain coroutine doesn’t provide

Page 89: allocation aware futures asynchronous ... - Meeting C++Meeting C++ 2018. 2 Introduction About me Denis Blank ... Author of the continuable and function2 libraries Interested in: compiler

89

Questions?Thank you for your attention

/Naios/continuableDenis Blank <[email protected]>

MIT Licensed

/Naios/talks

slides code

me