mirror of
https://github.com/boostorg/cobalt.git
synced 2026-01-19 04:02:16 +00:00
216 lines
7.3 KiB
Plaintext
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]
|
|
|