In my previous post, “I hate RTOSes”, I have identified blocking as the main cause of the particular brittleness and inflexibility of the programs based on RTOSes. Here I’d like to discuss techniques of minimizing blocking and eradicating it completely from the application-level code. In other words, I’d like to show you how to use an RTOS for building responsive event-driven software.
For reasons I’ve outlined before, experienced RTOS users have learned to be weary of peppering the code with the blocking calls to the RTOS. So, even though every RTOS boasts a plethora of various communication and synchronization mechanisms (all of them based on blocking), advanced real-time developers intentionally limit their designs to just one generic blocking call per task, as shown in the following pseudocode:
void task_routine(void *arg) { while (1) { // block on any event designated for this task (generic) // process the event *without* further blocking (task specific) } }
Most RTOSes provide mechanisms to wait for multiple events in a single blocking call, for example: event flags, message mailboxes, message queues, the select()
call, condition variables, and many others. From all these possibilities, I’d like to single out the message queue, because it is the most generic and flexible mechanism. A message posted to a message queue not only unblocks any task that waits on the queue (synchronization), but the message can also contain any information associated with the event (interprocess communication). For example, a message from an analog-to-digital converter (ADC) can signal when the conversion has completed as well as the actual value of the conversion result.
The generic pseudocode of a task based on a message queue looks as follows:
void task_routine(void *arg) { while (1) { // main event loop of the task void *event = msg_queue_get(); // wait for event // process the event *without* further blocking (task specific) } }
The most important premise of this event-loop design is that the task-specific code that processes the events obtained from the queue is not allowed to block. The event-processing code must execute quickly and return back to the event loop, so that the event loop can check for other events.
This design also automatically guarantees that each event is processed in run-to-completion (RTC) fashion. By design, the event loop must necessarily complete processing of the current event before looping back to obtain and process the next event. Also note that the need for queuing events is an immediate consequence of the RTC processing style. Queuing prevents losing events that arrive while the event-loop is executing an RTC step.
The event-loop pseudocode shown above is still task-specific, but it is quite easy to make it completely generic. As shown below, you can combine a message queue and an event-handler pointer-to-function in the TCB structure. A pointer to the TCB struct can be then passed to the task in the argument of the task routine (arg
). This is quite easily achieved when the task is created.
typedef struct { MessageQueue queue; // event queue associated with the task void (*handler)(void *event); // event handler pointer-to-function } TCB; // task control block void task_routine(void *arg) { while (1) { // main event loop of the task void *event = msg_queue_get(((TCB *)arg)->queue); // wait for event (*((TCB *)arg)->handler)(event);// handle the event without blocking } }
The last snippet of code is generic, meaning that this simple event-loop can be used for all tasks in you application. So at this point, you can consider the task_routine()
function as part of the generic event-driven infrastructure for executing your applications, which consist of event-handler functions.
What this way of thinking gives you is quite significant, because in fact you have just created your first event-driven framework.
The distinction between a framework and a toolkit is simple. A toolkit, such as an RTOS, is essentially a collection of functions that you can call. When you use a toolkit, you write the main body of the application (such as all the task routines) and you call the various functions from the RTOS. When you use a framework, you reuse the main body (such as the task_routine()
function) and you provide the code that the framework calls. In other words, a framework uses inverted control compared to a traditional RTOS.
Inversion of control is a very common phenomenon in all event-driven architectures, because it recolonizes the plain fact that the events are controlling the application, not the other way around.
In my next post in the “I hate RTOSes” series, I’ll talk about challenges of programming without blocking. I’ll explain what you need to sacrifice when you write non-blocking code and why this often leads to “spaghetti” code. Stay tuned!