QP/C 8.1.2
Real-Time Event Framework
Loading...
Searching...
No Matches
Active Objects

OverviewEvents

Concepts & Definitions

As described in the Overview, the main goal of the QP/C 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).

Active Object Model of Computation

The Active Object Model of Computation represents a paradigm shift compared to the traditional "shared state concurrency" based on a traditional Real-Time Operating System (RTOS) with hard-coding various blocking calls to the RTOS and managing shared resources by explicit mutual exclusion. 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

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/C Application consisting of multiple, event-driven Active Objects that collectively deliver the desired functionality:

Figure SRS-AOS: Block diagram showing communicating Active Objectss in QP

Encapsulation for Concurrency

The traditional object-oriented encapsulation, as provided by C++, C#, or Java, does not 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 (e.g., RTOS 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 in threads or increased latency for ISRs), causes contention, and often leads to missed real-time deadlines [Sutter:10]. Also, this style of managing concurrency is inherently unsafe because developers might 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 hazards.

Figure SRS-SEQ-TH: Threads interacting (synchronously) with a passive object

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.

Figure SRS-SEQ-AO: Threads interacting (asynchronously) with an Active Object

Shared-Nothing Principle

Encapsulation for concurrency is not a programming language feature, so it is no more difficult to achieve in C than in C++. Still, 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.

Attention
QP/C Framework by itself cannot and is not required to guarantee or enforce the shared-noting principle. Achieving the strict no-sharing of resources among Active Objects is the responsibility of the QP/C Applications. But at the very least, the QP/C Framework architecture and design must provide adequate and thread-safe event-based information exchange mechanisms, which can replace the traditional sharing of data or resources.

Execution Context

In the UML, Active Object is defined as: "the object having its thread of control" [UML 2.5]. A traditional thread might indeed be used for Active Objects when the QP/C Framework runs on top of a conventional 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.)

Priority

The execution context of an Active Object is closely related to its priority relative to other Active Objects or "naked" threads in the system. An Active Object can be viewed primarily as a priority level for executing the functionality it encapsulates. In the QP/C Framework, each Active Object is required to have a unique priority. (See also SRS_QP_AO_01.)

Run-to-Completion (RTC)

Each Active Object processes events in run-to-completion (RTC) fashion, which must be guaranteed by the underlying QP/C 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.

Note
It is essential to clearly distinguish the notion of RTC from the concept of preemption [OMG 07]. In particular, RTC does not mean that the Active Object thread has to monopolize the CPU until the RTC step is complete. In fact, RTC steps can be preempted by interrupts or other threads executing on the same CPU. Such thread preemption is determined by the scheduling policy of the underlying real-time kernel, not by the Active Object model. When the preempted Active Object is assigned the CPU time again, it resumes its event processing from the point of preemption and, eventually, completes its RTC step. As long as the preempting and the preempted threads don't share any resources (see Encapsulation), there are no concurrency hazards.

Current Event

Current event is the event being processed in the run-to-completion (RTC) step. The event-driven infrastructure (QP/C Framework in this case) must guarantee that the current event remains available and unchanging for the whole duration of the RTC step, which might be preempted multiple times (by higher-priority processing).

No Blocking

Most traditional operating systems (both RTOS and general-purpose OS) 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 semantics. This is because every blocking call is really another way to deliver an event (event is delivered by unblocking and returning 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 in-line polling for events) means that the expected sequence of events becomes 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. (See also "Blocking==Technical Debt"↑.)

Event Queue

Each Active Object has its 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/C Framework in this case, is responsible for delivering and queuing the events in a deterministic and thread-safe manner.

Asynchronous Communication

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 (AO) but doesn't wait in line for the actual processing of the event. The QP/C 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 itself. All events are treated uniformly, regardless of their origin.

Note
When a preemptive real-time kernel executes Active Objects, event posting from a lower-priority AO to a higher-priority AO can lead to an immediate preemption of the lower-priority AO by the higher-priority AO before the end of the RTC step in the lower-priority AO. This behavior is indistinguishable from a synchronous function call, in which the caller (lower-priority AO) waits in line for processing of the posted event in the higher-priority AO. However, this is not voluntary blocking, but rather a special case of preemption determined by the choice of priorities and the particular scheduling policy of the underlying kernel. Also, as described above, this does not violate the RTC processing semantics of the lower-priority AO.

State Machines

Event-driven components, like Active Objects, must generally 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 maintained in some other way, which often leads to a multitude of variables and flags checked and modified in a convoluted if-then-else logic (a.k.a. "spaghetti code").

A well-known alternative to such "improvised context management" is the concept of a state machine, which manages the execution context using "states". QP/C Framework augments and complements the Active Object model by providing support state machines to represent the internal behavior of Active Objects.

The relationship between Active Objects and state machines is mutually synergistic. On one hand, the Active Objects provide the execution context and event queuing that the state machines need to process the events in a run-to-completion fashion. On the other hand, state machines provide the structure and clear design for the event-driven behavior running inside the Active Objects. State machines are also the most constructive part of the design, amenable to modeling and automatic code generation.

Remarks
One of the main advantages of combining the Active Object model with state machines in the QP/C Framework is that it raises the level of abstraction and provides the right abstractions to apply modeling and code generation because hierarchical state machines (UML statecharts supported in QP/C Framework) are known to be the most constructive part of the UML specification ([UML-2.5]). Also, state machines are one of the semi-formal methods highly recommended by the functional safety standards (e.g., [IEC 61508-3:2010] Part-7).

Inversion of Control

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 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/C Framework), rather than in the application. In other words, the control is inverted compared to a traditional sequential thread.

Framework vs. Toolkit

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

Low Power Architecture

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. 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-saving 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.

Requirements


SRS_QP_AO_00

QP/C Framework shall provide the Active Object abstraction to QP/C Application

Description
The Active Object abstraction provided by QP/C Framework shall be customizable by QP/C Application and executed by QP/C Framework according to the Active Object model of computation.

Use Case
QP/C Framework can meet this requirement by providing the Active Object abstraction as a class, which the QP/C Applications can customize by subclassing. Such an Active Object base class must be customizable at compile-time for a wide variety of real-time kernels, including traditional blocking RTOS and event-driven, non-blocking kernels.

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_01

QP/C Framework shall be able to manage a compile-time configurable number of Active Objects not exceeding 64 instances.

Description
The maximum number of Active Object instances managed by QP/C Framework at any given time shall be compile-time configurable with a maximum of 64 instances. The actual number of Active Object instances registered with the QP/C Framework at runtime can be lower than the configured compile-time maximum, but it cannot exceed that maximum.

Use Case
For best memory and CPU performance, the maximum number of Active Object instances can be configured below the 64 maximum, and often it might be advantageous to use powers of 2; for example, configure the maximum as 16 or 32 AO instances.

Forward Traceability (truncated to 2 level(s))

  • SAS_QP_MEM_01: Active Object memory allocation policy and responsibilities
    • SDS_QA_START: QA Application startup sequence
    • SSM_QA_CSOU_41: Mandate: QP/C Application must allocate all objects statically.
  • SAS_QP_MEM_03: Active Object stack allocation policy and responsibilities
    • SSM_QA_CSOU_41: Mandate: QP/C Application must allocate all objects statically.



SRS_QP_AO_10

Active Object abstraction shall provide the unique priority for each Active Object instance

Description
The Active Object priority is a positive integer number between 1 and the maximum number of Active Objects configured at compile-time (see SRS_QP_AO_01).

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_11

Active Object priority shall use the direct priority numbering scheme

Description
The Active Object priority shall conform to the direct numbering scheme defined as follows: priority 1 corresponds to the lowest priority, and higher numbers correspond to higher priorities (direct priority numbering scheme). The priority zero cannot be assigned to any Active Object, and is reserved for the idle thread (or the idle condition) of the underlying real-time kernel.

Use Case
The QP priority numbering scheme is fixed and does not change, even if the underlying real-time kernel uses a different priority scheme (e.g., reversed priority numbering). However, the QP priority must remain consistent with the priority of the underlying kernel in that higher QP priority numbers must correspond to a higher (or at least not lower) urgency threads.

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_12

Active Object abstraction may provide a second "auxiliary priority" for each Active Object instance

Description
The Active Object's "auxiliary priority" may be used for different purposes, depending on the underlying real-time kernel.

Use Cases
In some real-time kernels, the "auxiliary priority" might be used to represent the preemption threshold (e.g., the preemptive QK kernel). In other kernels (e.g., 3rd-party RTOS kernels), it might represent the native thread priority according to the priority numbering scheme of the kernel.

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_20

Active Object abstraction shall provide an event queue for each Active Object instance

Description
The event queue type shall be compile-time configurable to allow various blocking/non-blocking mechanisms corresponding to the chosen real-time kernel. The capacity of the event queue (maximum number of events it can hold) shall be run-time configurable before the Active Object instance starts executing.

Use Case
QP/C Framework can meet this requirement by providing an event queue as an attribute inside the Active Object class. The type of the event queue must be compile-time configurable to match the underlying real-time kernel that executes the Active Objects in QP.

Forward Traceability (truncated to 2 level(s))

  • SRS_QP_AO_21: Active Object event queue shall provide FIFO policy for posting events from outside the Active Object
    • SRS_QP_EDM_00: QP/C Framework shall provide direct event posting to Active Object instances based on the FIFO policy.
    • SRS_QP_EDM_01: QP/C Framework shall provide direct event self-posting to Active Object instances based on the LIFO policy
  • SRS_QP_AO_22: Active Object event queue shall additionally provide LIFO policy for self-posting events from within the Active Object
  • SRS_QP_AO_23: The maximum capacity of the Active Object event queue shall be run-time configurable
  • SRS_QP_EDM_00: QP/C Framework shall provide direct event posting to Active Object instances based on the FIFO policy.
  • SRS_QP_EDM_01: QP/C Framework shall provide direct event self-posting to Active Object instances based on the LIFO policy



SRS_QP_AO_21

Active Object event queue shall provide FIFO policy for posting events from outside the Active Object

Description
FIFO stands for First-In-First-Out and means that the events are extracted from the queue in the same order in which they have been inserted into the queue.

Backward Traceability

  • SRS_QP_AO_20: Active Object abstraction shall provide an event queue for each Active Object instance

Forward Traceability (truncated to 2 level(s))

  • SRS_QP_EDM_00: QP/C Framework shall provide direct event posting to Active Object instances based on the FIFO policy.
  • SRS_QP_EDM_01: QP/C Framework shall provide direct event self-posting to Active Object instances based on the LIFO policy



SRS_QP_AO_22

Active Object event queue shall additionally provide LIFO policy for self-posting events from within the Active Object

Description
LIFO stands for Last-In-First-Out and means that the events are extracted from the queue in the reversed order in which they have been inserted into the queue. The support for the LIFO policy for self-posting should be in addition to supporting the standard FIFO policy. In other words, the QP/C Application can choose to self-post any given event either with the FIFO or LIFO policy.

Backward Traceability

  • SRS_QP_AO_20: Active Object abstraction shall provide an event queue for each Active Object instance

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_23

The maximum capacity of the Active Object event queue shall be run-time configurable

Description
The maximum capacity of an event queue is the maximum number of events that can be inside the queue at any given time. This maximum capacity shall be determined at run-time.

Use Case
The event queue can be supplied with the buffer memory to hold the events at run-time. The size of that memory buffer will determine the maximum capacity of the event queue.

Backward Traceability

  • SRS_QP_AO_20: Active Object abstraction shall provide an event queue for each Active Object instance

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_30

Active Object abstraction may provide an optional execution context for each Active Object instance

Description
The execution context (e.g., thread) attribute shall be compile-time configurable to allow various thread types corresponding to the chosen real-time kernel.

Use Case
In case QP uses a traditional RTOS, the execution context might be a thread-control-block (TCB) or just a thread handle/pointer. In the case of other real-time kernels, the execution context might be a thread identifier or might not be needed at all.

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_31

QP/C Framework shall allow Active Object instances to be started at runtime

Description
Starting an Active Object instance means registering it with the QP/C Framework, so that the framework can start managing the Active Object. Only after an Active Object instance has been started can it receive and process events. Starting at runtime means that Active Object instances can begin at any time during the regular system operation (as opposed to starting only during system initialization).

Use Case
Starting an Active Object instance might involve initializing its event queue and creating and/or starting the execution context. Only after that, the underlying real-time kernel can include the Active Object in the scheduling process.

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_32

QP/C Framework may allow Active Object instances to be stopped at runtime

Description
Stopping an Active Object instance means stopping its execution context and unregistering it from the QP/C Framework. Stopping an Active Object does not mean that it is deleted, destroyed, or that its memory is recycled. Stopping only means that the QP/C Framework no longer manages the Active Object and stops participating in scheduling and event processing.

Background
The biggest challenge in stopping an Active Object is to perform it cleanly, without disrupting the rest of the application. For example, the stopped Active Object should not receive any events from the rest of the application. Also, the Active Object should not have any events to process in its queue. For these reasons, stopping an Active Object is optional, and is not recommended, especially in safety-related applications. Instead of stopping an Active Object, a better design is to post a special event to that Active Object, which could trigger a transition to a special "stopped" state in the Active Object's state machine.

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_40

Active Object abstraction may provide an optional "operating-system object" for each Active Object instance

Description
The optional "operating system object" shall be compile-time configurable to allow various operating-system object types corresponding to the selected real-time kernel.

Use Case
For example, the Active Object's event-queue for the POSIX operating system might require an additional condition-variable attribute ("operating system object") to implement blocking on an empty queue.

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_50

Active Object abstraction shall encapsulate its internals

Description
The Active Object abstraction should hide and protect its internals, both for reading and writing by any outside entities. Additionally, the QP/C Framework shall allow the QP/C Application to hide and protect any additional attributes added to the derived Active Objects.

Forward Traceability (truncated to 2 level(s))

  • SRS_QP_AO_51: Active Object abstraction shall allow Applications to access the internal attributes from inside the Active Object easily



SRS_QP_AO_51

Active Object abstraction shall allow Applications to access the internal attributes from inside the Active Object easily

Description
Notwithstanding Requirement SRS_QP_AO_10, the QP/C Framework shall allow for easy and computationally inexpensive access to the internal attributes of an Active Object from within the AO, such as from its internal state machine.

Use Case
A good example of implementing such a policy is the concept of class encapsulation in OOP, where the internal attributes are accessible to the class operations (e.g., via the this pointer) and are harder to access from the outside.

Backward Traceability

  • SRS_QP_AO_50: Active Object abstraction shall encapsulate its internals
  • SRS_QP_SM_24: All State Machine Implementation Strategies provided by QP shall allow Applications to easily access the instance variables associated with a given state machine object

Forward Traceability (truncated to 2 level(s))



SRS_QP_AO_60

Active Object abstraction shall support run-to-completion event processing

Description
QP/C Framework must guarantee that every event is processed to completion, regardless of the real-time kernel used, and that during the RTC step, the current event remains available and unchanged.



SRS_QP_AO_70

Active Object abstraction shall provide support for state machines.

Description
Each Active Object instance shall have an internal state machine, with the features and semantics specified in Section State Machines. QP/C Framework shall guarantee execution of such internal state machines in a Run-to-Completion fashion.

Backward Traceability

  • SRS_QP_SM_00: QP/C Framework shall provide support for hierarchical state machines for Active Objects and for passive stateful objects in the QP/C Application.

Forward Traceability (truncated to 2 level(s))


OverviewEvents