213 lines
6.6 KiB
Markdown
213 lines
6.6 KiB
Markdown
|
# Crash Course: cooperative scheduler
|
||
|
|
||
|
<!--
|
||
|
@cond TURN_OFF_DOXYGEN
|
||
|
-->
|
||
|
# Table of Contents
|
||
|
|
||
|
* [Introduction](#introduction)
|
||
|
* [The process](#the-process)
|
||
|
* [Adaptor](#adaptor)
|
||
|
* [The scheduler](#the-scheduler)
|
||
|
<!--
|
||
|
@endcond TURN_OFF_DOXYGEN
|
||
|
-->
|
||
|
|
||
|
# Introduction
|
||
|
|
||
|
Sometimes processes are a useful tool to work around the strict definition of a
|
||
|
system and introduce logic in a different way, usually without resorting to the
|
||
|
introduction of other components.
|
||
|
|
||
|
`EnTT` offers a minimal support to this paradigm by introducing a few classes
|
||
|
that users can use to define and execute cooperative processes.
|
||
|
|
||
|
# The process
|
||
|
|
||
|
A typical process must inherit from the `process` class template that stays true
|
||
|
to the CRTP idiom. Moreover, derived classes must specify what's the intended
|
||
|
type for elapsed times.
|
||
|
|
||
|
A process should expose publicly the following member functions whether needed
|
||
|
(note that it isn't required to define a function unless the derived class wants
|
||
|
to _override_ the default behavior):
|
||
|
|
||
|
* `void update(Delta, void *);`
|
||
|
|
||
|
It's invoked once per tick until a process is explicitly aborted or it
|
||
|
terminates either with or without errors. Even though it's not mandatory to
|
||
|
declare this member function, as a rule of thumb each process should at
|
||
|
least define it to work properly. The `void *` parameter is an opaque pointer
|
||
|
to user data (if any) forwarded directly to the process during an update.
|
||
|
|
||
|
* `void init();`
|
||
|
|
||
|
It's invoked when the process joins the running queue of a scheduler. This
|
||
|
happens as soon as it's attached to the scheduler if the process is a top
|
||
|
level one, otherwise when it replaces its parent if the process is a
|
||
|
continuation.
|
||
|
|
||
|
* `void succeeded();`
|
||
|
|
||
|
It's invoked in case of success, immediately after an update and during the
|
||
|
same tick.
|
||
|
|
||
|
* `void failed();`
|
||
|
|
||
|
It's invoked in case of errors, immediately after an update and during the
|
||
|
same tick.
|
||
|
|
||
|
* `void aborted();`
|
||
|
|
||
|
It's invoked only if a process is explicitly aborted. There is no guarantee
|
||
|
that it executes in the same tick, this depends solely on whether the
|
||
|
process is aborted immediately or not.
|
||
|
|
||
|
Derived classes can also change the internal state of a process by invoking
|
||
|
`succeed` and `fail`, as well as `pause` and `unpause` the process itself. All
|
||
|
these are protected member functions made available to be able to manage the
|
||
|
life cycle of a process from a derived class.
|
||
|
|
||
|
Here is a minimal example for the sake of curiosity:
|
||
|
|
||
|
```cpp
|
||
|
struct my_process: entt::process<my_process, std::uint32_t> {
|
||
|
using delta_type = std::uint32_t;
|
||
|
|
||
|
my_process(delta_type delay)
|
||
|
: remaining{delay}
|
||
|
{}
|
||
|
|
||
|
void update(delta_type delta, void *) {
|
||
|
remaining -= std::min(remaining, delta);
|
||
|
|
||
|
// ...
|
||
|
|
||
|
if(!remaining) {
|
||
|
succeed();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
delta_type remaining;
|
||
|
};
|
||
|
```
|
||
|
|
||
|
## Adaptor
|
||
|
|
||
|
Lambdas and functors can't be used directly with a scheduler for they are not
|
||
|
properly defined processes with managed life cycles.<br/>
|
||
|
This class helps in filling the gap and turning lambdas and functors into
|
||
|
full-featured processes usable by a scheduler.
|
||
|
|
||
|
The function call operator has a signature similar to the one of the `update`
|
||
|
function of a process but for the fact that it receives two extra arguments to
|
||
|
call whenever a process is terminated with success or with an error:
|
||
|
|
||
|
```cpp
|
||
|
void(Delta delta, void *data, auto succeed, auto fail);
|
||
|
```
|
||
|
|
||
|
Parameters have the following meaning:
|
||
|
|
||
|
* `delta` is the elapsed time.
|
||
|
* `data` is an opaque pointer to user data if any, `nullptr` otherwise.
|
||
|
* `succeed` is a function to call when a process terminates with success.
|
||
|
* `fail` is a function to call when a process terminates with errors.
|
||
|
|
||
|
Both `succeed` and `fail` accept no parameters at all.
|
||
|
|
||
|
Note that usually users shouldn't worry about creating adaptors at all. A
|
||
|
scheduler creates them internally each and every time a lambda or a functor is
|
||
|
used as a process.
|
||
|
|
||
|
# The scheduler
|
||
|
|
||
|
A cooperative scheduler runs different processes and helps managing their life
|
||
|
cycles.
|
||
|
|
||
|
Each process is invoked once per tick. If it terminates, it's removed
|
||
|
automatically from the scheduler and it's never invoked again. Otherwise it's
|
||
|
a good candidate to run one more time the next tick.<br/>
|
||
|
A process can also have a child. In this case, the parent process is replaced
|
||
|
with its child when it terminates and only if it returns with success. In case
|
||
|
of errors, both the parent process and its child are discarded. This way, it's
|
||
|
easy to create chain of processes to run sequentially.
|
||
|
|
||
|
Using a scheduler is straightforward. To create it, users must provide only the
|
||
|
type for the elapsed times and no arguments at all:
|
||
|
|
||
|
```cpp
|
||
|
entt::scheduler<std::uint32_t> scheduler;
|
||
|
```
|
||
|
|
||
|
It has member functions to query its internal data structures, like `empty` or
|
||
|
`size`, as well as a `clear` utility to reset it to a clean state:
|
||
|
|
||
|
```cpp
|
||
|
// checks if there are processes still running
|
||
|
const auto empty = scheduler.empty();
|
||
|
|
||
|
// gets the number of processes still running
|
||
|
entt::scheduler<std::uint32_t>::size_type size = scheduler.size();
|
||
|
|
||
|
// resets the scheduler to its initial state and discards all the processes
|
||
|
scheduler.clear();
|
||
|
```
|
||
|
|
||
|
To attach a process to a scheduler there are mainly two ways:
|
||
|
|
||
|
* If the process inherits from the `process` class template, it's enough to
|
||
|
indicate its type and submit all the parameters required to construct it to
|
||
|
the `attach` member function:
|
||
|
|
||
|
```cpp
|
||
|
scheduler.attach<my_process>(1000u);
|
||
|
```
|
||
|
|
||
|
* Otherwise, in case of a lambda or a functor, it's enough to provide an
|
||
|
instance of the class to the `attach` member function:
|
||
|
|
||
|
```cpp
|
||
|
scheduler.attach([](auto...){ /* ... */ });
|
||
|
```
|
||
|
|
||
|
In both cases, the return value is an opaque object that offers a `then` member
|
||
|
function to use to create chains of processes to run sequentially.<br/>
|
||
|
As a minimal example of use:
|
||
|
|
||
|
```cpp
|
||
|
// schedules a task in the form of a lambda function
|
||
|
scheduler.attach([](auto delta, void *, auto succeed, auto fail) {
|
||
|
// ...
|
||
|
})
|
||
|
// appends a child in the form of another lambda function
|
||
|
.then([](auto delta, void *, auto succeed, auto fail) {
|
||
|
// ...
|
||
|
})
|
||
|
// appends a child in the form of a process class
|
||
|
.then<my_process>(1000u);
|
||
|
```
|
||
|
|
||
|
To update a scheduler and therefore all its processes, the `update` member
|
||
|
function is the way to go:
|
||
|
|
||
|
```cpp
|
||
|
// updates all the processes, no user data are provided
|
||
|
scheduler.update(delta);
|
||
|
|
||
|
// updates all the processes and provides them with custom data
|
||
|
scheduler.update(delta, &data);
|
||
|
```
|
||
|
|
||
|
In addition to these functions, the scheduler offers an `abort` member function
|
||
|
that can be used to discard all the running processes at once:
|
||
|
|
||
|
```cpp
|
||
|
// aborts all the processes abruptly ...
|
||
|
scheduler.abort(true);
|
||
|
|
||
|
// ... or gracefully during the next tick
|
||
|
scheduler.abort();
|
||
|
```
|