== Custom Executors One of the reasons cobalt defaults to https://www.boost.org/doc/libs/1_83_0/doc/html/boost_asio/reference/any_io_executor.html::[`asio::any_io_executor`] is that it is a type-erased executor, i.e. you can provide your own event-loop without needing to recompile `cobalt`. However, during the development of the Executor TS, the executor concepts got a bit unintuitive, to put it mildly. Ruben Perez wrote an excellent https://anarthal.github.io/cppblog/asio-props.html::[blog post], which I am shamelessly going to draw from. === Definition An executor is a type that points to the actual event loop and is (cheaply) copyable, which supports properties (see below) is equality comparable and has an `execute` function. ==== `execute` [source,cpp] ---- struct example_executor { template void execute(Fn && fn) const; }; ---- The above function executes `fn` in accordance with its properties. ==== Properties A property can be queried, preferred or required, e.g.: [source,cpp] ---- struct example_executor { // get a property by querying it. asio::execution::relationship_t &query(asio::execution::relationship_t) const { return asio::execution::relationship.fork; } // require an executor with a new property never_blocking_executor require(const execution::blocking_t::never_t); // prefer an executor with a new property. the executor may or may not support it. never_blocking_executor prefer(const execution::blocking_t::never_t); // not supported example_executor prefer(const execution::blocking_t::always_t); }; ---- ==== Properties of the `asio::any_io_executor` In order to wrap an executor in an `asio::any_io_executor` two properties are required: - `execution::context_t - `execution::blocking_t::never_t` That means we need to either make them require-able (which makes no sense for context) or return the expected value from `query`. The `execution::context_t` query should return `asio::execution_context&` like so: [source,cpp] ---- struct example_executor { asio::execution_context &query(asio::execution::context_t) const; }; ---- The execution context is used to manage lifetimes of services that manage lifetimes io-objects, such as asio's timers & sockets. That is to say, by providing this context, all of asio's io works with it. NOTE: The `execution_context` must remain alive after the executor gets destroyed. The following may be preferred: - `execution::blocking_t::possibly_t` - `execution::outstanding_work_t::tracked_t` - `execution::outstanding_work_t::untracked_t` - `execution::relationship_t::fork_t` - `execution::relationship_t::continuation_` That means you might want to support them in your executor for optimizations. // thanks @anarthal ==== The `blocking` property As we've seen before, this property controls whether the function passed to `execute()` can be run immediately, as part of `execute()`, or must be queued for later execution. Possible values are: * `asio::execution::blocking.never`: never run the function as part of `execute()`. This is what `asio::post()` does. * `asio::execution::blocking.possibly`: the function may or may not be run as part of `execute()`. This is the default (what you get when calling `io_context::get_executor`). * `asio::execution::blocking.always`: the function is always run as part of `execute()`. This is not supported by `io_context::executor`. ==== The `relationship` property `relationship` can take two values: * `asio::execution::relationship.continuation`: indicates that the function passed to `execute()` is a continuation of the function calling `execute()`. * `asio::execution::relationship.fork`: the opposite of the above. This is the default (what you get when calling `io_context::get_executor()`). Setting this property to `continuation` enables some optimizations in how the function gets scheduled. It only has effect if the function is queued (as opposed to run immediately). For `io_context`, when set, the function is scheduled to run in a faster, thread-local queue, rather than the context-global one. ==== The `outstanding_work_t` property `outstanding_work` can take two values: * `asio::execution::outstanding_work.tracked`: indicates that while the executor is alive, there's still work to do. * `asio::execution::outstanding_work.untracked`: the opposite of the above. This is the default (what you get when calling `io_context::get_executor()`). Setting this property to `tracked` means that the event loop will not return as long as the `executor` is alive. === A minimal executor With this, let's look at the interface of a minimal executor. [source,cpp] ---- struct minimal_executor { minimal_executor() noexcept; asio::execution_context &query(asio::execution::context_t) const; static constexpr asio::execution::blocking_t query(asio::execution::blocking_t) noexcept { return asio::execution::blocking.never; } template void execute(F && f) const; bool operator==(minimal_executor const &other) const noexcept; bool operator!=(minimal_executor const &other) const noexcept; }; ---- NOTE: See https://github.com/boostorg/cobalt/tree/master/example/python.cpp[example/python.cpp] for an implementation using python's `asyncio` event-loop. === Adding a work guard. Now, let's add in a `require` function for the `outstanding_work` property, that uses multiple types. [source,cpp] ---- struct untracked_executor : minimal_executor { untracked_executor() noexcept; constexpr tracked_executor require(asio::execution::outstanding_work:: tracked_t) const; constexpr untracked_executor require(asio::execution::outstanding_work::untracked_t) const {return *this; } }; struct untracked_executor : minimal_executor { untracked_executor() noexcept; constexpr tracked_executor require(asio::execution::outstanding_work:: tracked_t) const {return *this;} constexpr untracked_executor require(asio::execution::outstanding_work::untracked_t) const; }; ---- Note that it is not necessary to return a different type from the `require` function, it can also be done like this: [source,cpp] ---- struct trackable_executor : minimal_executor { trackable_executor() noexcept; constexpr trackable_executor require(asio::execution::outstanding_work:: tracked_t) const; constexpr trackable_executor require(asio::execution::outstanding_work::untracked_t) const; }; ---- If we wanted to use `prefer` it would look as shown below: [source,cpp] ---- struct trackable_executor : minimal_executor { trackable_executor() noexcept; constexpr trackable_executor prefer(asio::execution::outstanding_work:: tracked_t) const; constexpr trackable_executor prefer(asio::execution::outstanding_work::untracked_t) const; }; ---- === Summary As you can see, the property system is not trivial, but quite powerful. Implementing a custom executor is a problem category of its own, which is why this documentation doesn't do that. Rather, there is an example of how to wrap a python event loop in an executor. Below are some reading recommendations. - https://cppalliance.org/richard/2020/10/31/RichardsOctoberUpdate.html[Richards October 2020 Update - container a qt-executor] - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0443r13.html[A Unified Executors Proposal for C++ | P0443R13] - https://www.boost.org/doc/libs/master/doc/html/boost_asio/std_executors.html[Asio's documentation on std executors]