Pipelines
The simplest implementation of ECS will run systems sequentially one after the other. Perhaps you are familiar with the idea of fixing your timestep. Simply running systems sequentially in a predefined order blows past that concept.
The solution is to have pipelines. Then you can have a pipeline for physics, one pipeline for rendering, and another for input, for example. Or as many as you would want. Each pipeline has a different configuration. The first criteria is, of course, how often to run the pipeline.
My suggestion is have pipelines run sequentially, one after the other. Then you can run the systems in the current pipeline concurrently. That way you know that only the systems of the current pipeline are running. For instance, the systems in the rendering pipeline know that the systems in the physics pipeline will not move entities (update their position components) because that is a different pipeline, and pipelines run sequentially.
With that said, if you need to, for example, split the physics pipeline in two, to make sure one stage is completed before running the other, that is ok. Furthermore if a pipeline ends up having only one system, that is ok too. Use this to ensure that you will not have systems stepping on each other.
Read-Write Locks
You can use pipelines to have control over which systems can run concurrently, which allows you to prevent running in parallel systems that may step on each other. However, that can be error prone, as it requires to design you pipelines carefully. There is, of course, another way to ensure this: locks.
As you might know, having system declare what kinds of component they need on initialization is a good idea. It allows some optimizations. For example, if a system wants entities that have two kinds of component, we have a couple options:
- We can go over every entity and checking if they have the components, every time the system runs.
- We can keep a list of the entities that match the criteria, and every time one of those components is added or removed we can update that list.
The latter is only possible if the system declares the kinds of components it wants before hand. Plus, multiple systems that want the same set of component kinds can share list. You might also be interested in the concept of Archetypes. Although I prefer to think about these as queries.
Ok, extend that idea to include not only which kinds of components the system will read, but also which kinds of components the system will write (we can assume that write access implies read access). And now you can consider read-write locks on kinds of components:
- If a system that wants to write a kind of component is running, you cannot run any systems that want to read or write that same kind of component at the same time.
- Otherwise, multiple systems that want to read (but not write) the same kind of component can run at the same time. Any system that wants to write that kind of component must wait.
On running a system for multiple entities in parallel
A system runs over the components sequentially. One of the talking points of ECS is that they store components contiguously in memory, and to really take advantage of that you want to run over them orderly.
However, that does not mean that running a system in parallel is imposible. Yet, in order to do so we need to consider whether or not that introduces race conditions. A solution is to have a flag indicating if a system should run in parallel or sequentially.