[−][src]Crate specs_task
A multitasking module that supports the fork-join model. Implemented on top of SPECS ECS.
Here we expound on the technical details of this module's implementation. For basic usage, see the tests.
In this model, every task is some entity. The entity is allowed to have exactly one component
that implements TaskComponent
(it may have other components that don't implement
TaskComponent
). The task will be run to completion by the corresponding TaskRunnerSystem
.
Every task entity is also a node in a (hopefully acyclic) directed graph. An edge t2 --> t1
means that t2
cannot start until t1
has completed.
In order for tasks to become unblocked, the TaskManagerSystem
must run, whence it will
traverse the graph, starting at the "final entities", and check for entities that have
completed, potentially unblocking their parents. In order for a task to be run, it must be the
descendent of a final entity. Entities become final by calling TaskManager::finalize
.
Edges can either come from SingleEdge
or MultiEdge
components, but you should not use these
types directly. You might wonder why we need both. It's a fair question, because adding the
SingleEdge
concept does not actually make the model capable of representing any semantically
new graphs. The reason is efficiency.
If you want to implement a fork join like this:
r#" ---> t1.1 ---
/ \
t2 ----> t1.2 -----> t0
\ /
---> t1.3 --- "#;
You would actually do this by calling TaskManager::make_fork
to create a "fork" entity called
F
that doesn't have a TaskComponent
, but it has a SingleEdge
from t2
to t0
, and a
MultiEdge
from t2
to { t1.1, t1.2, t1.3 }
. Note that the children on the MultiEdge
are
called "prongs" of the fork.
r#" t2 --> F --> t0
|
| --> t1.1
| --> t1.2
| --> t1.3 "#;
The semantics would be such that this graph is equivalent to the one above. Before any of the
tasks connected to F
by the MultiEdge
could run, the task connected by the SingleEdge
(t0
) would have to be complete. t2
could only run once all of the children of F
had
completed.
The advantages of this scheme are:
- a traversal of the graph starting from
t2
does not visit the same node twice - it is a bit easier to create fork-join graphs with larger numbers of concurrent tasks
- there are fewer edges for the most common use cases
Every user of this module should use it via the TaskManager
. It will enforce certain
invariants about the kinds of entities that can be constructed. For example, any entity with a
MultiEdge
component is considered a "fork entity", and it is not allowed to have a
TaskComponent
or a TaskProgress
. Therefore, if you want a task to have multiple children, it
must do so via a fork entity.
These systems must be dispatched for tasks to make progress:
TaskManagerSystem
TaskRunnerSystem
for everyT: TaskRunner
used
This module can be dangerous when used improperly due to the dynamic nature of SPECS. Potential bugs not handled by this module:
- leaked orphan entities
- graph cycles
- DO NOT manually touch the storages for task module components! Always go through the
TaskManager
.
Structs
FinalTag | WARNING: only public because |
MultiEdge | WARNING: only public because |
SingleEdge | WARNING: only public because |
TaskManager | The main object for users of this module. Manages all non-background task operations. |
TaskManagerSystem | Traverses all descendents of all finalized entities and unblocks them if possible. |
TaskProgress | WARNING: only public because |
TaskRunnerSystem | The counterpart to an implementation |
Enums
UnexpectedEntity | This error means the entity provided to one of the APIs did not have the expected components. |
Traits
TaskComponent | An ephemeral component that needs access to |