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: