For these and other reasons, the more experienced software developers
learned to be very weary of blocking and instead
they apply a set of best practices to reduce the blocking in their software.
From the historical perspective, these best practices combine the RTOS,
object-oriented programming, and event-driven programming. I will call these trends "Modern",
as in the title of this presentation. So, for the purposes of this talk "Modern
Embedded Systems Programming" means anything invented after the mid 1980's.
This is in contrast to the still dominating "Traditional" approach, based on a traditional
RTOS and shared-state-concurrency that dates back to the early 1980's when RTOSes went mainstream.
The great Danish quantum physicist, Niels Bohr, used to define an "Expert"
as a man who has made all the mistakes which can be made, in a narrow field".
In this sense, experts in the filed of concurrency noticed that the use of
naked threads and the multitude of blocking mechanisms often leads to systems that are
unresponsive, difficult to reason about and often unsafe.
Instead, software experts from various industries, independently started to
arrive at the same conclusions about safer and more robust embedded software architecture.
These "modern" approaches, universally favor strictly encapsulated threads
that don't share anything, are allowed to communicate only by exchanging events or messages,
and that use blocking only to wait for events, but otherwise don't block at all.
Please note that the best practices don't dismiss the RTOS. The "threads" are still there. But while
a naked RTOS allows you to do anything, the "modern" approaches go beyond the RTOS,
by restricting the use of threads to a specific set of best practices.
One of the most succinct formulations of these best practices I found, comes from the C++ and
concurrency expert Herb Sutter. Specifically, his article "Prefer Using Active Objects Instead
of Naked Threads" contains the following three best practices, which I rephrase here as follows:
One: Keep data isolated, private and bound to threads:
This means: Avoid sharing of data or resources among threads. This eliminates the "shared-state
concurrency" node in the "perils of blocking" diagram. Without sharing,
there is no need for mutual exclusion and the big part of the vicious circle is eliminated.
Of course, "avoid sharing" is easy to say,
but how are then the threads supposed to communicate and synchronize?
Well, this is addressed in the best practice number two:
Communicate among threads via asynchronous events.
This best practice packs two important concepts:
"events" or "messages" and "asynchronous communication".
"Event objects", also called "messages", are packaged data specially designed for
communication. They carry the information about what happened: for example a "timeout-occurred",
or "button1-was-pressed", or "button2-was-released".
Additionally, event objects can carry event parameters that pertain to the event. For example,
an "ethernet-packet-arrived" event can carry the whole ethernet data packet as its event parameter.
You will see examples of events in a minute.
"Asynchronous communication" means that senders of events only post the events to the recipients,
but they don't wait in-line, meaning don't block, until the event is processed.
So, the best practice number two is designed to eliminate
the "synchronization by blocking" node from the "perils of blocking diagram".
And finally, the best practice number three: Organize your threads around "message pumps",
goes directly after the blocking node in the diagram.
Please note that the "blocking" node cannot be eliminated completely,
at least not with a traditional RTOS, because as you remember,
every RTOS thread must necessarily block somewhere in its "superloop".
But the best practice number three prescribes the only allowed structure of the "superloop"
as the "message pump", which restricts the blocking to only one specific place in the loop.
So, let's take a closer look at an "event loop", also known as a "message pump", implemented here
with the FreeRTOS API. The basic architecture is very simple: it consists of the usual for-ever
"superloop" with just two steps: waiting for an event, and dispatch the event for processing.
The waiting for events is implemented with a message queue that *blocks* when no events
are available. Shown here in red, this is the only place in the loop that blocks.
Once an event is received, the event-loop processes the event in the dispatch call,
which is NOT allowed to block. This is the most important restriction of the event-loop
with the most far reaching consequences.
The first consequence of the NO BLOCKING restriction is that dispatch must process
one event at a time, to completion, and necessarily *return* back to the event loop in
order to start processing the next event. This is called Run-To-Completion (RTC) event processing.
The second consequence is that dispatch cannot be coded sequentially with the
usual wait-respond paradigm. This would not only clog the event loop and make it unresponsive,
but it would also violate the Run-To-Completion event processing requirement.
How so? Well, any instance of blocking really represents waiting for an event and unblocking
represents reception of the awaited event. But such an event would then arrive through
the back-door of blocking in the middle of processing the original event from the queue. So,
you would now process two events at a time, thus violating the RTC semantics of event processing.
And finally, the third consequence of event-loop is that it leads to *inversion of control*
compared to the traditional RTOS.
In an RTOS, *you* write the body of each and every thread, and *you*,
meaning the application developer, choose which RTOS blocking mechanisms you employ in your code.
Event-loop reverses it, because it predetermines the structure of every thread
and *it* calls your code – the dispatch code.
Moreover, you are no longer allowed to use any RTOS blocking mechanisms in that code.
Of course, all these best practices combined establish a design pattern,
which is known as the "Active Object" pattern.
In this pattern, Active Objects, like any other objects, have
private data. But each active object also has a private thread and a private event queue.
The only way to interact with an active object is by posting events to its event queue.
The posting is *asynchronous*, meaning that the event is only placed in the queue,
but there is no waiting until the event is processed.
The event processing happens in the event-loop running in the private
thread of the active object. The processing might
involve sending secondary events to other active objects or even to self.
In a sense active objects are the most stringent form of object-oriented programming,
because the asynchronous communication enables active objects to be truly encapsulated.
In contrast, the traditional OOP encapsulation, as provided by C++, C# or Java, does not really
encapsulate anything in terms of concurrency. Any operation on an object runs in the caller's thread
and the object's private data are subject to the same race conditions as global data,
not encapsulated at all. To become thread-safe, operations need to be explicitly protected by a
mutual exclusion mechanism, such as a mutex or a monitor, but this reduces parallelism
dramatically, causes contention, and is a natural enemy of scalability.
In contrast, all private data of an active object are truly encapsulated for concurrency without
any mutual exclusion mechanism, because they can be only accessed from the active object's
own thread. Note that this encapsulation for concurrency is not a programming language feature,
so it is no more difficult to achieve in C as in C++, but it requires a programming
discipline to avoid sharing resources (known as the "shared-nothing principle").
However, the event-based communication helps immensely, because instead of sharing a resource,
a dedicated active object can become the manager or broker of that resource and the
rest of the system can access the resource only via events posted to this broker active object.
The Active Object pattern is valuable, because it naturally implements
and automatically enforces the best practices of concurrent programming.
The pattern also dramatically improves your ability to reason about your thread's code
and operation by giving you higher-level abstractions and idioms that raise the
semantic level of your program and let you express your intent more directly and safely,
thus improving your productivity.
As most good ideas in software, the concept of autonomous software objects communicating by
message passing is not new. It can be traced back to the 1970s when Carl Hewitt at MIT
came up with the Actor model of computation.
That's why the "Active Object" pattern is sometimes also called the "Actor" model.
In the 1990s, methodologies like Real-Time Object-Oriented Modeling
(ROOM) adapted actors for real-time computing.
More recently, Unified Modeling Language (UML)
introduced the concept of Active Objects that are essentially synonymous with the ROOM actors.
Both variants use hierarchical state machines (UML statecharts)
to model the internal behavior of active objects. I'll talk about state machines a bit later.