C++23 executors¶
concore implements C++23 executors, as defined by P0443: A Unified Executors Proposal for C++. It is not a complete implementation, but the main features are present. Concore’s implementation includes:
concepts
customization point objects
thread pool
type wrappers
However, it does not include:
properties and requirements – they seem too complicated to be actually needed
extra conditions for customization point object behavior; i.e., a scheduler does not automatically become a sender – the design for this is too messy, with too many circular dependencies
Concepts and customization-point objects¶
The following table lists the customization-point objects (CPOs) defined:
CPO |
Description |
---|---|
|
Given a receiver |
|
Given a receiver |
|
Given a receiver |
|
Executes a functor in an executor |
|
Connects the given sender with the given receiver, resulting an |
|
Starts an |
|
Submit a sender and a receiver for execution |
|
Given a scheduler, returns a sender that can kick-off a chain of |
|
Bulk-executes a functor N times in the context of an executor. |
The following table lists the concepts defined:
Concept |
Description |
---|---|
|
Indicates that the given type can execute work of type |
|
Indicates that the given type can execute work of the given |
|
Indicates that the given type is a bare-bone receiver. That is, |
|
Indicates that the given type is a receiver. That is, it supports |
|
Indicates that the given type is a sender. |
|
Indicates that the given type is a typed sender. |
|
Indicates that the given type |
|
Indicates that the given type is an operation state. |
|
Indicate that the given type is a scheduler. That is |
Concepts, explained¶
executor¶
A C++23 executor
concept matches the way concore looks at an executor: it is able to schedule work. To be noted that all concore executors (global_executor
, spawn_executor
, inline_executor
, etc.) fulfill the executor
concept.
The way that P0443 defines the concept, an executor
is able to execute any type of functor compatible with void()
. While a task
is a type compatible with void()
, concore ensures that all the executors have a specialization that takes directly task
. This is done mostly for type erasure, helping compilation times.
If ex
is of type E
that models concept executor
, then the one can perform work on that executor with a code similar to:
concore::execute(ex, [](){ do_work(); });
operation_state¶
An operation_state
object is essentially a pair between an executor
object and a task
object.
Given an operation op
of type Oper
, one can start executing it with a code like:
concore::start(op);
An operation is typically obtained from a sender
object and a receiver
object by calling the connect
CPO:
operation_state auto op = concore::connect(snd, recv);
scheduler, sender¶
A scheduler
is an agent that is capable of starting asynchronous computations. Most often a scheduler is created out of an executor
object, but there is no direct linkage between the two.
A scheduler
object can start asynchronous computations by creating a sender
object. Given a sched
object that matches the z``scheduler`` concept, then one can obtain a sender in the following way:
sender auto snd = concore::schedule(sched);
A sender
object is an object that performs some asynchronous operation in a given execution context. To use a sender
, one must always pair it with are receiver
, so that somebody knows about the operation being completed. As shown, above, this pairing can be done with the connect
function. Thus, putting them all together, one one can start a computation from a scheduler
if there is a receiver
object to collect the results, as shown below:
receiver recv = ...
sender auto snd = concore::schedule(sched);
operation_state auto op = concore::connect(snd, recv);
concore::start(op);
To skip the intermediate step of creating an operation_state
, one might call submit
, that essentially combines connect
and start
:
receiver recv = ...
sender auto snd = concore::schedule(sched);
concore::submit(snd, recv);
Also, a sender
can be directly created from an executor
by using the as_sender
type wrapper:
receiver recv = ...
sender auto snd = concore::as_sender(ex);
concore::submit(snd, recv);
receiver¶
A receiver
is the continuation of an asynchronous task. It is aways used to consume the results of a sender
.
One can create a receiver from an invocable object by using the as_receiver
wrapper:
auto my_fun = []() { on_task_done(); }
receiver recv = concore::as_receiver(my_fun)
The division between a receiver
and a sender
is a bit blurry. One can add computations that need to be executed asynchronously in any of them. Moreover, one can construct objects that are both receiver
and sender
at the same type. This is useful to create chains of computations.
Type wrappers¶
The following diagrams shows the type wrappers and how they transform different types of objects: