Task Synchronisation – Part 2: Multiple Tasks and RTOS APIs
So far we have only dealt with the simple case of two task synchronisation. We now address the case where multiple tasks are waiting at the synchronisation point:
In the design of real-time systems it is common for a task to synchronise on a number of different conditions. This many involve a conjunction (AND) or disjunction (OR) of events. For example, a motor fault condition may be specified as:
- low oil pressure AND > 30sec after start-up
- Isobutane present in outlet line OR
- Isobutane present in front flush OR
- Isobutane present in rear flush
Given the array of synchronisation requirements and options how do modern RTOS support synchronisation? The majority of RTOSs support the following:
- Event Flags / Event Flag Groups
- Semaphores as Signals
EVENT FLAGS
Typically an Event Flag is implemented as a Manual-reset, Persistent, Unilateral synchronisation object. It is by far the simplest idea and mechanism, and is best suited to one-to-many (1:M) or many-to-one (1:M) synchronisation. An API may consist of the following calls:
- Set – sets the flag, readying any waiting tasks; can be called from an ISR
- Clear – clears the flag, if the flag is cleared then any arriving task is blocked
- Wait – non-consuming pend on a flag
- Set(group, bitmask)
- Clear(group, bitmask)
- Wait(group, bitmask, AND_OR, timeout)
RTOSs differ in the implementation of Event Flag Groups, with some only supporting M:1 synchronisation and not supporting 1:M or M:M synchronisation. In this case each event flag group is bound to a specific task (i.e. EFGs cannot stand as independent resources), altering the API to:
- Set(task_id, bitmask)
- Clear(task_id, bitmask)
- Wait( bitmask, AND_OR, timeout, &events;)
A surprising number of RTOSs do not support the concept of Event Flags, thus no support for any form of disjunctive or conjunctive synchronisation. The usual argument for not supporting them is that it can be difficult (if not impossible) to make timing deterministic (especially disjunction). Timing typically takes an O(N) form where N is the number of waiting tasks.
Some examples of event flag support from commercial RTOSs are:
-
>VxWorks
- 32 events in an event field
- Each task has its own events field that can be filled by having tasks and/or ISRs sending events to the task.
- ThreadX
- 32 event flags in a group
- Each event flag group is a public resource
- Nucleus PLUS
- 32 event flags in a group
- Each event flag group is a public resource
- uC/OS-III
- 8, 16 or 32 event flags in a group (compile time configured)
- Each event flag group is a public resource
The generic concept of a signal is for synchronisation between two tasks, with a simple API of:
- signal
- wait – timeout option
Most RTOSs, then, do not support the concept of a signal directly, instead directing the programmer to use the semaphore as a synchronisation object (the Semaphore as a Signal pattern). When using a semaphore as a signal, the SO takes the form of an Auto-reset Persistent Unilateral synchronisation object.
The Semaphore as a Signal pattern is regularly used to synchronise tasks with ISRs triggered by external events. This mechanism is favoured since the ISR will never block when posting to the semaphore (thus avoiding the potential to ‘stall’ the system).
If we have sporadic interrupts, then the ISR may signal the semaphore multiple times before the task waits on it. Does the application need to know only that the event has occurred, or does it need to know the actual number of times an event has occurred? Either is valid, depending on the system requirements. Note that most RTOSs use the counting semaphore as a signal and thus will count the number of events.
As an example, VxWorks does not support signals, but does support both the binary semaphore and the counting semaphore. Either can be used for synchronisation if created EMPTY (0). The following calls can be used by tasks to use the semaphore as a synchronisation object.
- semGive()
- Binary – giving a “full” semaphore has no effect
- Counting – increments count by one
- semTake()
- will block if count is zero
- semFlush()
- all waiting tasks are unblocked; semaphore left at zero
As already stated, one limitation of bilateral synchronisation (aka the Rendezvous) is that it cannot be used for ISR to task synchronisation . Because of this, bilateral synchronisation is rarely supported by commercial RTOSs. Notable, though, is the ITRON Project, which creates standards for real-time operating systems used in embedded systems (µITRON4.0 Specification). Like the Ada programming language it supports the concept of the Rendezvous for bilateral synchronisation (it actually uses the term Rendezvous Ports).
So far we have considered general synchronisation between two or more tasks. There is one further synchronisation use case we need to examine. Take, for example, the C code for a simple stack shown below. These routines use the variable count to keep track of the stack size. If the stack is empty then pop returns STACK_EMPTY and the caller must check for and take appropriate error handling actions.
Suppose that we do not want to return STACK_EMPTY, but want to wait (synchronise) for the stack to contain data. Since the waiting task owns the mutex no other task will be able to enter the critical region and push an element onto the stack. Thus the waiting task could never be signalled the stack is no longer empty – a deadlock.
SUMMARY
When designing software for embedded systems that is relying on a particular Real-Time Operating System, it is essential to understand that the behaviour of synchronisation primitives differ from RTOS to RTOS. Many questions need answering such as:
- Does the RTOS support only unilateral synchronisation, or does it include primitives for bilateral synchronisation?
- If multiple tasks are waiting then how many are readied?
- If only one, then how is it selected (priority / FIFO)?
- Is the signal persistent or non-persistent?
- Is the synchronisation object manual-reset or auto-reset?
- Does the RTOS support multiple event Conjunction and/or Disjunction?
November 19th, 2009 at 2:03 pm
Barriers are not commonly defined as you have stated here. They are usually a generalisation of a rendezvous (and so bilateral) — all (involved) tasks will wait when they reach a barrier. When the last task reaches the barrier, all are released. This is *not* what semFlush does!
Also, since the last task to reach the barrier does the signalling, and this can be any of the tasks, it is effectively a many-to-many synchronisation.
–
I'm glad you added the note about bilateral rendezvous with two semaphores, but it is better to signal (post) first and then wait (pend) in *both* tasks. Mixing the order as you have will result in scenarios with one more context switch than is necessary. It's just an efficiency thing.
–
I'm glad to see you mention condition variables, but you didn't say anything about blocking/non-blocking varieties. That specifies whether the 'signal' operation will block or not ('wait' can always block).