As described in the Overview, the main goal of the QP Framework is to provide a lightweight and efficient implementation of the Active Object Model of Computation with the specific focus on real-time embedded (RTE) systems, such as single-chip microcontroller units (MCUs).
The Active Object Model of Computation represents a paradigm shift compared to the traditional "shared state concurrency" based on explicit mutual exclusion and managing threads by blocking. The following subsections describe Active Object properties and explain why this model of computation is safer and makes it easier to write correct concurrent software.
Active Objects (a.k.a., Actors) are autonomous software objects, each possessing an event queue and execution context. They encapsulate state and behavior and communicate asynchronously by exchanging events. Figure SRS-AOS below shows a QP Application consisting of multiple, event-driven Active Objects that collectively deliver the desired functionality:
The traditional object-oriented encapsulation, as provided by C++, C# or Java, does not really encapsulate anything in terms of concurrency. As shown in Figure SRS-SEQ-TH, any operation on a passive object still runs in the caller's thread. If that passive object is shared among multiple threads, the object's attributes are subject to the same race conditions as global data, not encapsulated at all. To become concurrency-safe, operations need to be explicitly protected by an appropriate mutual exclusion mechanism, such as a mutex (for threads) or a critical section (for Interrupt Service Routines). However, this reduces responsiveness (blocking or increased latency), causes contention, and often leads to missed real-time deadlines [Sutter:10]. Also this style of managing concurrency is inherently unsafe because developers might simply forget to apply mutual exclusion or use an incorrect mechanism, which can lead to latent, highly intermittent, hard-to-find and hard-to-fix concurrency bugs.
In contrast, as shown in Figure SRS-AOS, all interactions with an Active Object occur by posting events, which are all handled in the execution context of the Active Object. As long as there is no sharing of data or resources among Active Objects (or any other concurrent entities), there are no concurrency hazards. Also, because each event is processed to completion (see Run-to-Completion processing), event processing is naturally serialized. This means that an Active Object is truly encapsulated without any mutual exclusion mechanisms. In this sense Active Objects are the most stringent form of object-oriented programming because they enable strict encapsulation for concurrency, which is much safer than the "naked" threads.
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 on behalf of the application developers to avoid sharing resources ("shared-nothing" principle). However, the event-based communication helps immensely, because instead of sharing a resource, a dedicated Active Object can become the manager/broker of the resource and the rest of the system can access the resource only via events posted to this broker Active Object.
In the UML, Active Object is defined as: "the object having its own thread of control" [UML 2.5]. A traditional thread might indeed be used for Active Objects when the QP Framework runs on top of a traditional multitasking kernel (e.g., traditional RTOS or general-purpose OS).
However, the Active Object model of computation can also work with real-time kernels that don't necessarily support the notion of traditional blocking threads. Active Objects have no need for blocking while handling events, which opens up possibilities of using lightweight, non-blocking kernels that might be non-preemptive or fully preemptive. (See also lightweight kernels provided in QP, such as Non-Preemptive Kernel, Preemptive Non-Blocking Kernel, and Preemptive Dual-Mode Kernel.)
The execution context of an Active Object is closely related to its priority relative to other Active Objects or "naked" threads in the system. In fact, an Active Object can be viewed primarily as a priority level for executing the functionality it encapsulates. In the QP Framework, each Active Object is required to have a unique priority. (See also SRS_QP_AO_01.)
Each Active Object has its own event queue and receives all events exclusively through that queue. This means that the event queue has only a single consumer (the Active Object that owns the queue). On the other hand, the event queue must accommodate multiple producers that don't need to be only Active Objects, but also interrupts (ISRs), or other software components (see Figure SRS-AOS). The Active Object infrastructure, such as the QP Framework in this case, is responsible for delivering and queuing the events in a deterministic and thread-safe manner.
All events are delivered to Active Objects asynchronously, meaning that an event producer merely posts an event to the event queue of the recipient Active Object but doesn't wait in line for the actual processing of the event.
The QP Framework makes no distinction between external events generated from interrupts and internal events originating from Active Objects. As shown in Figure SRS-AOS, an Active Object can post events to any other Active Object, including to self. All events are treated uniformly, regardless of their origin.
Each Active Object processes events in run-to-completion (RTC) fashion, which must be guaranteed by the underlying QP Framework. RTC means that Active Objects process the events one at a time and the next event can only be processed after the previous event has been processed completely. RTC event processing is the essential requirement for proper execution of state machines.
Current event is the event being processed in the run-to-completion (RTC) step. The event-driven infrastructure (QP Framework in this case) must guarantee that the current event remains available and unchanging for the whole duration of the RTC step.
Most traditional operating systems manage the threads and all inter-thread communication based on blocking, such as waiting on a semaphore or a time-delay. However, blocking (as in the middle of the RTC step) is incompatible with the RTC event processing requirement. This is because every blocking call is really another way to deliver an event (event is delivered by unblocking and return from a blocking call). Such "backdoor" event delivery happening in the middle of the RTC step violates the RTC semantics, because after unblocking the Active Object needs to process two events at a time (the original one and the new one delivered by unblocking).
Another detrimental consequence of blocking (or polling for events) inside RTC steps is that Active Objects become unresponsive to events delivered to their event queues. This, in turn, can cause Active Objects to miss their hard-real time deadlines and also can cause overflow of the internal event queue.
Finally, blocking (or polling for events) means that the expected sequences of events are hard-coded, which is inherently inflexible and not extensible, especially if the system must handle multiple event sequences (which turns out to be the case in most real-life systems).
Event-driven components, like Active Objects, must often retain the execution context from one event to the next. This must be done without blocking, so the context can't be preserved on the call-stack as it is done in the traditional, blocking RTOS threads. Instead, the context between events must be preserved in some other way, which often leads to multitude of variables and flags checked and modified in a convoluted if-then-else logic (a.k.a. "spaghetti code").
A well know alternative to such "improvised context management" is the concept of a state machine, which manages the execution context using "states". QP Framework augments and complements the Active Object model of computation by providing support for state machines to represent the internal behavior of Active Objects.
Event-driven systems require a distinctly different way of thinking than traditional sequential threads. When a sequential thread needs some incoming event, it explicitly blocks and waits in-line until the event arrives. Thus the sequential thread remains "in control" at all times, but while waiting for one kind of event, it cannot respond (at least for the time being) to any other events.
In contrast, most event-driven applications are structured according to the Hollywood principle, which essentially means "Don't call us, we'll call you." So, an event-driven Active Object is not in control while waiting for an event; in fact, it's not even active. Only once the event arrives, the event-driven program is called to process the event and then it quickly relinquishes the control again. This arrangement allows an event-driven program to wait for many events in parallel, so the system remains responsive to all events it needs to handle. This scheme implies that in an event-driven system the control resides within the event-driven infrastructure (QP Framework), rather than in the application. In other words, the control is inverted compared to a traditional sequential thread.
Inversion of control is the key property that makes a software framework different from a software toolkit. A toolkit, such as a traditional RTOS, is essentially a set of predefined functions that you can call. When you use a toolkit, you write the main body of the application, such as the body of all RTOS threads, and call the various blocking functions from the RTOS. When you use a framework (such as QP), you reuse the main body (codified inside the framework) and provide the application code that it calls, so the control resides in the framework rather than in your code. Indeed, this inversion of control gives the event-driven infrastructure all the defining characteristics of a framework:
"One important characteristic of a framework is that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user's application code. The framework often plays the role of the main program in coordinating and sequencing application activity. This <u>inversion of control</u> gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application."
–Ralph Johnson and Brian Foote
Most modern embedded microcontrollers (MCUs) provide an assortment of low-power sleep modes designed to conserve power by gating the clock to the CPU and various peripherals. However, the sleep modes are entered under the software control and therefore require an appropriate software infrastructure.
An event-driven framework, like QP, based on inversion of control is particularly suitable for taking advantage of these power-savings features because the framework can easily detect situations in which the system has no more events to process, called the idle condition. In that case the framework can place the MCU into a low-power sleep mode safely and without creating race conditions with active interrupts.
QP Framework shall provide the Active Object abstraction to QP Application
QP Framework shall be able to manage a compile-time configurable number of Active Objects not exceeding 64 instances
Active Object abstraction shall provide the unique priority for each Active Object instance
Active Object abstraction may provide second "auxiliary priority" for each Active Object instance
Active Object abstraction shall provide an event queue for each Active Object instance
Active Object event queue shall provide FIFO policy for posting events from outside the Active Object
Active Object event queue shall additionally provide LIFO policy for self-posting events from within the Active Object
The maximum capacity of the Active Object event queue shall be run-time configurable
Active Object abstraction may provide an optional execution context for each Active Object instance
QP Framework shall allow Active Object instances to be started at runtime
QP Framework may allow Active Object instances to be stopped at runtime
Active Object abstraction may provide optional "operating-system object" for each Active Object instance
Active Object abstraction shall encapsulate its internals
Active Object abstraction shall allow Applications to easily access the internal attributes from inside the Active Object
this
pointer) and are harder to access from the outside. Active Object abstraction shall support run-to-completion event processing
Active Object abstraction shall provide support for state machines.