MIRA
Threading


Overview

Running code in independent threads, using a process method that operates in a certain interval, synchronizing threads or decoupling callbacks from worker code are frequently used when implementing drivers or data processing software modules.

MIRA aims to free the user from worrying about threads, thread synchronization and locking data from concurrent access. So one of the design goals of the framework was to take away the responsibility of managing threads, callbacks and decoupling from the normal or unexperienced user but giving the expert user all the flexibility and possibilities he needs.

This is why Authorities, MicroUnits and Units use an internal thread dispatcher to handle callbacks, rpc calls and timers without bothering the user with the details mentioned above. By default, an authority/unit creates its own thread that is used for channel data callbacks, rpc calls, initialization and timer methods. The user can rely on no function being called concurrently and can therefore spare the use of mutexes.

The thread dispatcher

The thread dispatcher class is a thread manager that allows the user to add different handlers, which get called from within the same thread. There are different handlers.

When the dispatcher is started, first all registered immediate handlers are called. Then the dispatcher starts by either waiting for signals to call a registered signal handler or waiting for a given duration until the next timer is due. Also when a new immediate handler is registered, it will be processed immediately. The dispatcher can be paused/stopped and resumed/started after initialisation has finished. Each time the dispatcher is stopped, all registered finalize handlers get called. By using a single thread for initialization, timers and destruction, it is possible to use external libraries that require that memory is allocated and destructed within the same thread as well as API calls are also made from the same thread where memory was allocated.

Authorities

Authorities use a built-in dispatcher. By default there are only two signal handlers registered, one for RPC calls and one for channel callbacks. The user can add initialization handlers as well as timers before calling Authority::start(). For more information please see Threads and Dispatchers.

MicroUnits

MicroUnits use this single threading model to add an initialization handler initialize() to the dispatcher that gets called before any of the signal handlers is called (i.e. channel data callbacks or rpc methods). The user can be sure that there will be no call to any of his data callback or rpc methods before the call to initialize() is finished. They also add a start handler that calls resume() each time the MicroUnit gets started, a stop handler that calls pause() each time the MicroUnit is stopped and a destruction handler that calls finalize() when the MicroUnit is going to be destroyed. When the user calls needRecovery(), a timer is registered that periodically calls recover() until the MicroUnit signals that it is operating normally again by calling operational().

Units

Additionally to MicroUnits, Units add a default timer to the dispatcher that periodically calls process(const Timer& timer) in a specific interval. This process() method also does not get not called until the call to initialize() is finished. The user can rely on that there will be no concurrent call to process(const Timer& timer), any data callback or any rpc method. So again no mutexes are required.

Timers

Timers are callbacks that will be called cyclically at a specified rate or once at a specified time. The user can schedule multiple timers at a thread dispatcher and they will get called within the dispatcher's thread whenever their invocation time is due and no other task is currently executed. This is why timers give no guarantee that they are called exactly in the given interval or at the scheduled time.

A callback function for a registered Timer must have the following interface:

void myTimerCallback(const Timer& timer)
{
}

The passed timer parameter contains information about the timer's current timing and execution times.

void myTimerCallback(const Timer& timer)
{
std::cout << timer.last; // time the last callback happened
std::cout << timer.currentExpected; // time the current callback should be happening
std::cout << timer.current; // time the current callback was actually called
std::cout << timer.lastDuration; //< How long did the last callback ran for
}

Timers can be added via the Authority, MicroUnit or Unit interface (see Threads and Dispatchers).