🛠️🐜 Antkeeper superbuild with dependencies included https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

212 lines
6.6 KiB

  1. # Crash Course: cooperative scheduler
  2. <!--
  3. @cond TURN_OFF_DOXYGEN
  4. -->
  5. # Table of Contents
  6. * [Introduction](#introduction)
  7. * [The process](#the-process)
  8. * [Adaptor](#adaptor)
  9. * [The scheduler](#the-scheduler)
  10. <!--
  11. @endcond TURN_OFF_DOXYGEN
  12. -->
  13. # Introduction
  14. Sometimes processes are a useful tool to work around the strict definition of a
  15. system and introduce logic in a different way, usually without resorting to the
  16. introduction of other components.
  17. `EnTT` offers a minimal support to this paradigm by introducing a few classes
  18. that users can use to define and execute cooperative processes.
  19. # The process
  20. A typical process must inherit from the `process` class template that stays true
  21. to the CRTP idiom. Moreover, derived classes must specify what's the intended
  22. type for elapsed times.
  23. A process should expose publicly the following member functions whether needed
  24. (note that it isn't required to define a function unless the derived class wants
  25. to _override_ the default behavior):
  26. * `void update(Delta, void *);`
  27. It's invoked once per tick until a process is explicitly aborted or it
  28. terminates either with or without errors. Even though it's not mandatory to
  29. declare this member function, as a rule of thumb each process should at
  30. least define it to work properly. The `void *` parameter is an opaque pointer
  31. to user data (if any) forwarded directly to the process during an update.
  32. * `void init();`
  33. It's invoked when the process joins the running queue of a scheduler. This
  34. happens as soon as it's attached to the scheduler if the process is a top
  35. level one, otherwise when it replaces its parent if the process is a
  36. continuation.
  37. * `void succeeded();`
  38. It's invoked in case of success, immediately after an update and during the
  39. same tick.
  40. * `void failed();`
  41. It's invoked in case of errors, immediately after an update and during the
  42. same tick.
  43. * `void aborted();`
  44. It's invoked only if a process is explicitly aborted. There is no guarantee
  45. that it executes in the same tick, this depends solely on whether the
  46. process is aborted immediately or not.
  47. Derived classes can also change the internal state of a process by invoking
  48. `succeed` and `fail`, as well as `pause` and `unpause` the process itself. All
  49. these are protected member functions made available to be able to manage the
  50. life cycle of a process from a derived class.
  51. Here is a minimal example for the sake of curiosity:
  52. ```cpp
  53. struct my_process: entt::process<my_process, std::uint32_t> {
  54. using delta_type = std::uint32_t;
  55. my_process(delta_type delay)
  56. : remaining{delay}
  57. {}
  58. void update(delta_type delta, void *) {
  59. remaining -= std::min(remaining, delta);
  60. // ...
  61. if(!remaining) {
  62. succeed();
  63. }
  64. }
  65. private:
  66. delta_type remaining;
  67. };
  68. ```
  69. ## Adaptor
  70. Lambdas and functors can't be used directly with a scheduler for they are not
  71. properly defined processes with managed life cycles.<br/>
  72. This class helps in filling the gap and turning lambdas and functors into
  73. full featured processes usable by a scheduler.
  74. The function call operator has a signature similar to the one of the `update`
  75. function of a process but for the fact that it receives two extra arguments to
  76. call whenever a process is terminated with success or with an error:
  77. ```cpp
  78. void(Delta delta, void *data, auto succeed, auto fail);
  79. ```
  80. Parameters have the following meaning:
  81. * `delta` is the elapsed time.
  82. * `data` is an opaque pointer to user data if any, `nullptr` otherwise.
  83. * `succeed` is a function to call when a process terminates with success.
  84. * `fail` is a function to call when a process terminates with errors.
  85. Both `succeed` and `fail` accept no parameters at all.
  86. Note that usually users shouldn't worry about creating adaptors at all. A
  87. scheduler creates them internally each and every time a lambda or a functor is
  88. used as a process.
  89. # The scheduler
  90. A cooperative scheduler runs different processes and helps managing their life
  91. cycles.
  92. Each process is invoked once per tick. If it terminates, it's removed
  93. automatically from the scheduler and it's never invoked again. Otherwise it's
  94. a good candidate to run one more time the next tick.<br/>
  95. A process can also have a child. In this case, the parent process is replaced
  96. with its child when it terminates and only if it returns with success. In case
  97. of errors, both the parent process and its child are discarded. This way, it's
  98. easy to create chain of processes to run sequentially.
  99. Using a scheduler is straightforward. To create it, users must provide only the
  100. type for the elapsed times and no arguments at all:
  101. ```cpp
  102. entt::scheduler<std::uint32_t> scheduler;
  103. ```
  104. It has member functions to query its internal data structures, like `empty` or
  105. `size`, as well as a `clear` utility to reset it to a clean state:
  106. ```cpp
  107. // checks if there are processes still running
  108. const auto empty = scheduler.empty();
  109. // gets the number of processes still running
  110. entt::scheduler<std::uint32_t>::size_type size = scheduler.size();
  111. // resets the scheduler to its initial state and discards all the processes
  112. scheduler.clear();
  113. ```
  114. To attach a process to a scheduler there are mainly two ways:
  115. * If the process inherits from the `process` class template, it's enough to
  116. indicate its type and submit all the parameters required to construct it to
  117. the `attach` member function:
  118. ```cpp
  119. scheduler.attach<my_process>(1000u);
  120. ```
  121. * Otherwise, in case of a lambda or a functor, it's enough to provide an
  122. instance of the class to the `attach` member function:
  123. ```cpp
  124. scheduler.attach([](auto...){ /* ... */ });
  125. ```
  126. In both cases, the return value is an opaque object that offers a `then` member
  127. function to use to create chains of processes to run sequentially.<br/>
  128. As a minimal example of use:
  129. ```cpp
  130. // schedules a task in the form of a lambda function
  131. scheduler.attach([](auto delta, void *, auto succeed, auto fail) {
  132. // ...
  133. })
  134. // appends a child in the form of another lambda function
  135. .then([](auto delta, void *, auto succeed, auto fail) {
  136. // ...
  137. })
  138. // appends a child in the form of a process class
  139. .then<my_process>(1000u);
  140. ```
  141. To update a scheduler and therefore all its processes, the `update` member
  142. function is the way to go:
  143. ```cpp
  144. // updates all the processes, no user data are provided
  145. scheduler.update(delta);
  146. // updates all the processes and provides them with custom data
  147. scheduler.update(delta, &data);
  148. ```
  149. In addition to these functions, the scheduler offers an `abort` member function
  150. that can be used to discard all the running processes at once:
  151. ```cpp
  152. // aborts all the processes abruptly ...
  153. scheduler.abort(true);
  154. // ... or gracefully during the next tick
  155. scheduler.abort();
  156. ```