2
0
mirror of https://github.com/boostorg/cobalt.git synced 2026-01-19 04:02:16 +00:00
Files
cobalt/doc/background/custom_executors.adoc
Klemens Morgenstern 60e5c163f6 [io] sleep
2025-06-24 18:15:10 +08:00

216 lines
7.3 KiB
Plaintext

== Custom Executors
One of the reasons cobalt defaults to https://www.boost.org/doc/libs/master/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<typename Fn>
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<class F>
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]