1 # Crash Course: cooperative scheduler
8 * [Introduction](#introduction)
9 * [The process](#the-process)
11 * [The scheduler](#the-scheduler)
13 @endcond TURN_OFF_DOXYGEN
18 Sometimes processes are a useful tool to work around the strict definition of a
19 system and introduce logic in a different way, usually without resorting to the
20 introduction of other components.
22 `EnTT` offers a minimal support to this paradigm by introducing a few classes
23 that users can use to define and execute cooperative processes.
27 A typical process must inherit from the `process` class template that stays true
28 to the CRTP idiom. Moreover, derived classes must specify what's the intended
29 type for elapsed times.
31 A process should expose publicly the following member functions whether
32 required (note that it isn't required to define a function unless the derived
33 class wants to _override_ the default behavior):
35 * `void update(Delta, void *);`
37 It's invoked once per tick until a process is explicitly aborted or it
38 terminates either with or without errors. Even though it's not mandatory to
39 declare this member function, as a rule of thumb each process should at
40 least define it to work properly. The `void *` parameter is an opaque pointer
41 to user data (if any) forwarded directly to the process during an update.
45 It's invoked when the process joins the running queue of a scheduler. This
46 happens as soon as it's attached to the scheduler if the process is a top
47 level one, otherwise when it replaces its parent if the process is a
52 It's invoked in case of success, immediately after an update and during the
57 It's invoked in case of errors, immediately after an update and during the
62 It's invoked only if a process is explicitly aborted. There is no guarantee
63 that it executes in the same tick, this depends solely on whether the
64 process is aborted immediately or not.
66 Derived classes can also change the internal state of a process by invoking
67 `succeed` and `fail`, as well as `pause` and `unpause` the process itself. All
68 these are protected member functions made available to be able to manage the
69 life cycle of a process from a derived class.
71 Here is a minimal example for the sake of curiosity:
74 struct my_process: entt::process<my_process, std::uint32_t> {
75 using delta_type = std::uint32_t;
77 void update(delta_type delta, void *) {
78 remaining -= std::min(remaining, delta);
88 delta_type remaining{1000u};
94 Lambdas and functors can't be used directly with a scheduler for they are not
95 properly defined processes with managed life cycles.<br/>
96 This class helps in filling the gap and turning lambdas and functors into
97 full featured processes usable by a scheduler.
99 The function call operator has a signature similar to the one of the `update`
100 function of a process but for the fact that it receives two extra arguments to
101 call whenever a process is terminated with success or with an error:
104 void(Delta delta, void *data, auto succeed, auto fail);
107 Parameters have the following meaning:
109 * `delta` is the elapsed time.
110 * `data` is an opaque pointer to user data if any, `nullptr` otherwise.
111 * `succeed` is a function to call when a process terminates with success.
112 * `fail` is a function to call when a process terminates with errors.
114 Both `succeed` and `fail` accept no parameters at all.
116 Note that usually users shouldn't worry about creating adaptors at all. A
117 scheduler creates them internally each and every time a lambda or a functor is
122 A cooperative scheduler runs different processes and helps managing their life
125 Each process is invoked once per tick. If it terminates, it's removed
126 automatically from the scheduler and it's never invoked again. Otherwise it's
127 a good candidate to run one more time the next tick.<br/>
128 A process can also have a child. In this case, the parent process is replaced
129 with its child when it terminates and only if it returns with success. In case
130 of errors, both the parent process and its child are discarded. This way, it's
131 easy to create chain of processes to run sequentially.
133 Using a scheduler is straightforward. To create it, users must provide only the
134 type for the elapsed times and no arguments at all:
137 entt::scheduler<std::uint32_t> scheduler;
140 It has member functions to query its internal data structures, like `empty` or
141 `size`, as well as a `clear` utility to reset it to a clean state:
144 // checks if there are processes still running
145 const auto empty = scheduler.empty();
147 // gets the number of processes still running
148 entt::scheduler<std::uint32_t>::size_type size = scheduler.size();
150 // resets the scheduler to its initial state and discards all the processes
154 To attach a process to a scheduler there are mainly two ways:
156 * If the process inherits from the `process` class template, it's enough to
157 indicate its type and submit all the parameters required to construct it to
158 the `attach` member function:
161 scheduler.attach<my_process>("foobar");
164 * Otherwise, in case of a lambda or a functor, it's enough to provide an
165 instance of the class to the `attach` member function:
168 scheduler.attach([](auto...){ /* ... */ });
171 In both cases, the return value is an opaque object that offers a `then` member
172 function to use to create chains of processes to run sequentially.<br/>
173 As a minimal example of use:
176 // schedules a task in the form of a lambda function
177 scheduler.attach([](auto delta, void *, auto succeed, auto fail) {
180 // appends a child in the form of another lambda function
181 .then([](auto delta, void *, auto succeed, auto fail) {
184 // appends a child in the form of a process class
188 To update a scheduler and therefore all its processes, the `update` member
189 function is the way to go:
192 // updates all the processes, no user data are provided
193 scheduler.update(delta);
195 // updates all the processes and provides them with custom data
196 scheduler.update(delta, &data);
199 In addition to these functions, the scheduler offers an `abort` member function
200 that can be used to discard all the running processes at once:
203 // aborts all the processes abruptly ...
204 scheduler.abort(true);
206 // ... or gracefully during the next tick