Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ericniebler/69a3a632e1d13f7d8f16e0fbd598e42f to your computer and use it in GitHub Desktop.

Select an option

Save ericniebler/69a3a632e1d13f7d8f16e0fbd598e42f to your computer and use it in GitHub Desktop.
A second executor compromise

Compromise V2

As discussed with Bryce and David.

The discussed compromise is that we should replace the enqueue functions with a limited set of lazy task factories. Any syntactic cost of providing a trivial output receiver to these operations can be hidden in wrapper functions. We do not expect a runtime cost assuming we also provide a trivial parameter-dropping receiver in the standard library that the implementation can optimize against.

My passing a full set of parameters to a task construction functions, any task we place in a task graph may be type erased with no loss of efficiency. There may still be some loss of efficiency if the executor is type erased before task construction because the compiler may no longer be able to see from the actual executor into the passed functions. The earlier we perform this operation, however, the more chance there is of making this work effectively.

The fundamental executor concepts

An executor should either be a sender of a sub executor, or a one way fire and forget entity. These can be implemented in terms of each other, and so can be adapted if necessary, potentially with some loss of information.

We may require or prefer to switch between these.

SenderExecutor

A SenderExecutor is a Sender and meets the requirements of Sender. The interface of the passed receiver should be noexcept.

Function Semantics
void submit(Receiver<OneWayExecutor::SubExecutorType) noexcept At some future point calls on_value(subExecutor) on the receiver on success, where subExecutor is *this or some subset of *this as useful. At some future point calls on_error(ErrorType on failure.
ExecutorType executor() noexcept Returns an executor. By default, *this.

submit and executor are required on an executor that has task constructors. Submit is a fundamental sender operation that may be called by a task at any point. To avoid deep recursion, a task may post itself directly onto the underlying executor, giving the executor a chance to pass a new sub executor. For example, if a prior task completes on one thread of a thread pool, the next task may reenqueue rather than running inline, and the thread pool may decide to post that task to a new thread. Hence, at any point in the chain the sub executor passed out of the executor may be utilized.

OneWayExecutor

The passed function may or may not be noexcept. The behavior on an exception escaping the passed task is executor-defined.

Function Semantics
void execute(void(void)) At some future point calls the passed callable.

Task construction functions

An executor may provide one of the following task construction functions at a minimum. This minimal set is intended to cover the functionality in P0443.

We may require or prefer an executor with support for any of these task construction functions.

In all cases, an exception that escapes the passed function will be passed to the on_error function of the outgoing receiver.

Function Semantics
Sender<T'> make_value_task(Sender<T>, T'(T)) Accepts a sender that passes T or some Sender<T>, returns a Sender that will send T' or some Sender<T'>. Either T or T' may be void. If the Sender calls on_error(E) or done() then those operations will propagate from Sender'
Sender<T'> make_bulk_value_task(Sender<T>, T'(T), size_t ShF(), S SF(), R RF()) Accepts a sender that passes T or some Sender<T>, returns a Sender that will send T' or some Sender<T'>. Either T or T' may be void. If the Sender calls on_error(E) or done() then those operations will propagate from Sender'. Other parameters are as in P0443.

The type propagated through the graph here will be either T, as defined by the function, or a sender of T. The reason for this is that any executor may choose to wrap its value in some information - that information may be propagation of a sub executor, it may be asynchronous information so that the tasks can be greedily enqueued and run in some other context, out of band of calls to on_value. At any point the passed Sender<T> can take a receiver of T to collapse the Sender back into a value, providing compatibility with an arbitrary receiver and a terminal point for asynchronous work chaining.

Before and after

In the table below, where a future is returned this is represented by the promise end of a future/promise pair in the compromise version. Any necessary synchronization or storage is under the control of that promise/future pair, rather than the executor, which will often allow an entirely synchronization-free structure.

Before After
executor.execute(f) executor.execute(f)
executor.execute(f) executor.make_value_task(NoneSender{}, f).submit(SinkReceiver{})
fut = executor.two_way_execute(f) executor.make_value_task(NoneSender{}, f).submit(futPromise)
fut' = executor.then_execute(f, fut) executor.make_value_task(fut, f).submit(fut'Promise)
executor.bulk_execute(f, shf, sf) executor.make_bulk_value_task(NoneSender{}, f, shf, sf, [](){}).submit(SinkReceiver{})
fut = executor.bulk_twoway_execute(f, shf, sf, rf) executor.make_bulk_value_task(NoneSender{}, f, shf, sf, rf).submit(futPromise{})
fut' = executor.bulk_then_execute(f, shf, sf, rf, fut') executor.make_bulk_value_task(fut, f, shf, sf, rf).submit(fut'Promise{})

If a constructed task is type erased, then it may benefit from custom overloads for known trivial receiver types to optimize. If a constructed task is not type erased then the outgoing receiver will trivially inline.

We do not expect any performance difference between the two forms of the one way execute definition. The task factory-based design gives the opportunity for the application to provide a well-defined output channel for exceptions. For example, the provided output receiver could be an interface to a lock-free exception list if per-request exceptions are not required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment