Skip to content
NxtChg edited this page Mar 17, 2018 · 4 revisions

The original idea for tasks was to provide light-weight multi-threading that would allow to spawn/kill tasks quickly to perform specific actions.

Over the years, however, the code evolved to treat tasks like loosely coupled sub-modules (connected only by commands), responsible for different areas of the system.

This allowed to simplify the code and improve multi-threading performance.

So today Simcoin tasks are loaded once on startup and never die until the system finished working.

Each task can subscribe to one or more commands using the subscribe() method in the base Task class.

After that it will receive commands via the incoming()/outgoing() virtual methods.

It can also set a wake up time (in milliseconds from now) by calling the wake_me_in() method of the base class. When this time comes, the wake_up() virtual method will be called.

The wake_up() method is called from a special tasks thread, running with 5 ms tick time, so that's your wake up granularity.

Multi-threading

Each task is guaranteed to be used only by one thread at a time.

This means that if the CPU is currently in the wake_up() function, no commands will be received in parallel via incoming()/outgoing() functions until the CPU exits wake_up().

And vice versa, if you are processing a command inside incoming() or outgoing(), you can be sure that the task will not be awaken until you're done.

Additionally, only one command of each type can be processed at a time.

To avoid deadlocks, you can still receive commands with the same thread, though.

So, for example, if you send a command to some other task inside wake_up() then that task can call you back using any commands you are subscribed to. And of course, you can send commands to yourself as well.

Details

Each task has a critical section in its base class. It protects incoming(), outgoing() and wake_up() methods.

Each task chain that listens to a particular command type has its own critical section that ensures subscribing to commands will not break sending the commands of the same type.

Finally the Tasks class has one global critical section that protects task addition and subscription.

This rather complicated setup is there to make our main workhorse - tasks.send() - multi-threading safe and re-enterable.

Here's how the send() function uses the locks:

  • lock tasks.cs, find the chain this command belongs to, unlock tasks.cs
  • lock chain.cs while calling incoming()/outgoing() methods for each task descriptor
  • lock task.cs inside the descriptor before forwarding incoming()/outgoing() calls

As you can see, each send() results in at least 5 critical section locks. This might seem like a lot, but due to the lightness of critical sections, this is actually nothing compared to the benefits it provides for task concurrency.

Originally we only had one global critical section in the Tasks class and the code was simpler, but it also meant that any command stalled the whole system while it was being processed!