EnTT: Crash Course: cooperative scheduler

EnTT 3.16.0

Loading...
Searching...
No Matches
Crash Course: cooperative scheduler

# Table of Contents

- Introduction
- The process
  - Continuation
  - Shared process
- The scheduler

# Introduction

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 other component types.
EnTT offers minimal support to this paradigm by introducing a few classes used to define and execute cooperative processes.

# The process

A typical task inherits from the process class template. Derived classes also specify what the intended type for elapsed times is.

A process should implement the following member functions whether needed (note that it is not required to define a function unless the derived class wants to override the default behavior):

- void update(Delta, void *);

  This is invoked once per tick until a process is explicitly aborted or ends either with or without errors. 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 succeeded();

  This is invoked in case of success, immediately after an update and during the same tick.

- void failed();

  This is invoked in case of errors, immediately after an update and during the same tick.

- void aborted();

  This is invoked only if a process is explicitly aborted. There is no guarantee that it executes in the same tick. It depends solely on whether the process is aborted immediately or not.

A class can also change the state of a process by invoking succeed and fail, as well as pause and unpause the process itself.
All these are public member functions made available to manage the life cycle of a process easily.

Here is a minimal example for the sake of curiosity:

struct my_process: entt::process {
using allocator_type = typename entt::process::allocator_type;
using delta_type = typename entt::process::delta_type;
my_process(const allocator_type &allocator, delta_type delay)
: entt::process{allocator},
remaining{delay}
{}
void update(delta_type delta, void *) {
remaining -= std::min(remaining, delta);
// ...
if(!remaining) {
succeed();
}
}
private:
delta_type remaining;
};
entt::basic_process< std::uint32_t >::delta_type
std::uint32_t delta_type
Definition process.hpp:96
entt::basic_process< std::uint32_t >::allocator_type
Allocator allocator_type
Definition process.hpp:94
entt::basic_process< std::uint32_t >::succeed
void succeed() noexcept
Definition process.hpp:152
entt
EnTT default namespace.
Definition dense_map.hpp:22
entt::process
basic_process< std::uint32_t > process
Alias declaration for the most common use case.
Definition fwd.hpp:13

## Continuation

A process may be followed by other processes upon successful termination.
This pairing can be set up at creation time, keeping the processes conceptually separate from each other while still combining them at runtime:

my_process process{};
process.then<my_other_process>();
entt::basic_process::then
basic_process & then(Args &&...args)
Assigns a child process to run in case of success.
Definition process.hpp:222

This approach allows processes to be developed in isolation and combined to define complex actions.
For example, a delayed operation where a parent process (such as a timer) schedules a child process (the deferred task) once the time is over.

The then function also accepts lambdas, which are associated with a dedicated process internally:

process.then([](entt::process &proc, std::uint32_t delta, void *data) {
// ...
})

The lambda function is such that it accepts a reference to the process that manages it (to be able to terminate it, pause it and so on), plus the usual values also passed to the update function.

## Shared process

All processes inherit from std::enable_shared_from_this to allow sharing with the caller.
The returned smart pointer was created using the allocator associated with the scheduler and therefore all its processes. This same allocator is available by invoking get_allocator on the process itself.

As far as possible, sharing a process is not intended to allow the caller to manage it. This could actually compromise the proper functioning of the scheduler and the process itself.
Rather, the purpose is to allow the callers to save a valid reference to the process, allowing them to intervene in its lifecycle through calls like pause and the like.

# The scheduler

A cooperative scheduler runs different processes and helps manage their life cycles.

Each process is invoked once per tick. If it terminates, it is removed automatically from the scheduler, and it is never invoked again. Otherwise, it is a good candidate to run one more time the next tick.
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 is easy to create a 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:

entt::basic_scheduler<std::uint64_t> scheduler;
entt::basic_scheduler
Cooperative scheduler for processes.
Definition scheduler.hpp:36

Otherwise, the scheduler alias is also available for the most common cases. It uses std::uint32_t as a default type:

entt::scheduler scheduler{};
entt::scheduler
basic_scheduler< std::uint32_t > scheduler
Alias declaration for the most common use case.
Definition fwd.hpp:19

The class 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:

// checks if there are processes still running
const auto empty = scheduler.empty();
// gets the number of processes still running
entt::scheduler::size_type size = scheduler.size();
// resets the scheduler to its initial state and discards all the processes
scheduler.clear();
entt::basic_scheduler::empty
bool empty() const noexcept
Returns true if at least a process is currently scheduled.
Definition scheduler.hpp:132
entt::basic_scheduler::size
size_type size() const noexcept
Number of processes currently scheduled.
Definition scheduler.hpp:124
entt::basic_scheduler< std::uint32_t >::size_type
std::size_t size_type
Definition scheduler.hpp:48
entt::basic_scheduler::clear
void clear()
Discards all scheduled processes.
Definition scheduler.hpp:142

To attach a process to a scheduler, invoke the attach function with a process type and the arguments to use to construct it:

scheduler.attach<my_process>(_1000u);
entt::basic_scheduler::attach
type & attach(Args &&...args)
Schedules a process for the next tick.
Definition scheduler.hpp:154

The scheduler will also provide the process with its allocator as the first argument.
In case of lambdas or functors, the required signature is the one already seen for the then function of a process:

scheduler.attach([](entt::process &, std::uint32_t, void *){ /* ... */ });

In both cases, the newly created process is returned by reference and its then member function is used to create chains of processes to run sequentially.
As a minimal example of use:

// schedules a task in the form of a lambda function
scheduler.attach([](entt::process &, std::uint32_t, void *) {
// ...
})
// appends a child in the form of another lambda function
.then([](entt::process &, std::uint32_t, void *) {
// ...
})
// 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:

// 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);
entt::basic_scheduler::update
void update(const delta_type delta, void *data=nullptr)
Updates all scheduled processes.
Definition scheduler.hpp:183

In addition to these functions, the scheduler offers an abort member function that is used to discard all the running processes at once:

// aborts all the processes abruptly ...
scheduler.abort(true);
// ... or gracefully during the next tick
scheduler.abort();
entt::basic_scheduler::abort
void abort(const bool immediate=false)
Aborts all scheduled processes.
Definition scheduler.hpp:211

The argument passed to the abort function indicates whether execution should be stopped immediately or processes should be notified on the next tick.