Tasks cooperate to perform the required activities of the software. Synchronization is required between individual tasks. The Ada rendezvous provides a powerful mechanism for this synchronization.
|
In this section... 6.1.1 Tasks 6.1.2 Anonymous Task Types 6.1.3 Dynamic Tasks 6.1.4 Priorities 6.1.5 Delay Statements |
| Summary of Guidelines from this section |
------------------------------------------------------------------------
package Elevator_Objects is
...
type Elevator_States is (Moving, Idle, Stopped, At_Floor);
type Up_Down is (Up, Down);
---------------------------------------------------------------------
task type Elevators is
entry Initialize;
entry Close_Door;
entry Open_Door;
entry Stop;
entry Idle;
entry Start (Direction : in Up_Down);
entry Current_State (My_State : out Elevator_States;
Current_Location : out Float);
end Elevators;
---------------------------------------------------------------------
...
end Elevator_Objects;
------------------------------------------------------------------------ |
Multiple tasks that implement the decomposition of a large matrix multiplication algorithm is an example of an opportunity for real concurrency in a multi-processor target environment. In a single processor target environment this approach may not be justified.
A task that updates a radar display every 30 milliseconds is an example of a cyclic activity supported by a task.
A task that detects an over-temperature condition in a nuclear reactor and performs an emergency shutdown of the systems is an example of a task to support a high priority activity.
Resources shared between multiple tasks, such as devices and abstract data structures, require control and synchronization since their operations are not atomic. Drawing a circle on a display may require that many low level operations be performed without interruption by another task. A display manager would ensure that no other task accesses the display until all these operations are complete.
Language Ref Manual references: 9.1 Abort Statements, 9.2 Task Types and Task Objects
Buffer is static and has a name, but its type is
anonymous. Because it is declared explicitly, the task type Buffer_Manager is
not anonymous. Channel is static and has a name, and its type is not
anonymous. Like all dynamic objects, Encrypted_Packet_Queue.all is
essentially anonymous, but its type is not.
task Buffer; task type Buffer_Manager; type Replaceable_Buffer is access Buffer_Manager; ... Encrypted_Packet_Queue : Replaceable_Buffer; Channel : Buffer_Manager; ... Encrypted_Packet_Queue := new Buffer_Manager; ... |
The consistent and logical use of task types, when necessary, contributes to understandability. Identical tasks can be derived from a common task type. Dynamically allocated task structures are necessary when you must create and destroy tasks dynamically or when you must reference them by different names.
Language Ref Manual references: 9.2 Task Types and Task Objects
task type Radar_Track;
type Radar_Track_Pointer is access Radar_Track;
Current_Track : Radar_Track_Pointer;
---------------------------------------------------------------------
task body Radar_Track is
begin
loop
-- update tracking information
...
-- exit when out of range
delay 1.0;
end loop;
...
end Radar_Track;
---------------------------------------------------------------------
...
loop
...
-- Unless some code deals with non-null values of Current_Track,
-- (such as an array of existing tasks)
-- this assignment leaves the existing Radar_Track task running with
-- no way to signal it to abort or to instruct the system to
-- reclaim its resources.
Current_Track := new Radar_Track;
...
end loop; |
Allocated task objects referenced by access variables allow you to generate aliases; multiple references to the same object. Anomalous behavior can arise when you reference an aborted task by another name.
A dynamically allocated task that is not associated with a name (a "dropped pointer") cannot be referenced for the purpose of making entry calls, nor can it be the direct target of an abort statement (see Guideline 5.4.3).
Language Ref Manual references: 3.8 Access Types, 4.8 Allocators, 9.3 Task Execution - Task Activation
Priority to prioritize the service of entries.
task T1 ... pragma Priority (High) ... Server.Operation ... task T2 ... pragma Priority (Medium) ... Server.Operation ... task Server ... accept Operation ... |
T1 is blocked. Otherwise, T2 and Server may
never execute. If T1 is blocked, it is possible for T2 to reach its call to
Server's entry (Operation) before T1. Suppose this has happened and that T1
now makes its entry call before Server has a chance to accept T2's call.This is the timeline of events so far:
T1 blocks T2 calls Server.Operation T1 unblocks T1 calls Server.Operation Does Server accept the call from T1 or from T2? |
T1's call would be
accepted by Server before that of T2. However, entry calls are queued in
first-in-first-out (FIFO) order and not queued in order of priority.
Therefore, the synchronization between T1 and Server is not affected by T1's
priority. As a result, the call from T2 is accepted first. This is a form of
priority inversion.
A solution might be to provide an entry for a High priority user and an entry
for a Medium priority user.
---------------------------------------------------------------------
task Server is
entry Operation_High_Priority;
entry Operation_Medium_Priority;
...
end Server;
---------------------------------------------------------------------
task body Server is
begin
loop
select
accept Operation_High_Priority do
Operation;
end Operation_High_Priority;
else -- accept any priority
select
accept Operation_High_Priority do
Operation;
end Operation_High_Priority;
or
accept Operation_Medium_Priority do
Operation;
end Operation_Medium_Priority;
or
terminate;
end select;
end select;
end loop;
...
end Server;
--------------------------------------------------------------------- |
T1 still waits for one execution of Operation when
T2 has already gained control of the task Server. In addition, the approach
increases the communication complexity (see Guideline 6.2.6).
Priority allows relative priorities to be placed on tasks to
accomplish scheduling. Precision becomes a critical issue with hard-deadline
scheduling. However, there are certain problems associated with using
priorities that warrant caution.
Priority inversion occurs when lower priority tasks are given service while
higher priority tasks remain blocked. In the above example, this occurred
because entry queues are serviced in FIFO order, not by priority. There is
another situation referred to as a race condition. A program like the one in
the first example might often behave as expected as long as T1 calls
Server.Operation only when T2 is not already using Server.Operation or
waiting. You cannot rely on T1 always winning the race, since that behavior
would be due more to fate than to the programmed priorities. Race conditions
change when either adding code to an unrelated task or porting this code to a
new target. Task priorities are not a means of achieving mutual exclusion.
Arranging task bodies in order of priority will elaborate the higher priority tasks first.
Priorities are used to control when tasks run relative to one another. When both tasks are not blocked waiting at an entry, the highest priority task is given precedence. However, the most critical tasks in an application do not always have the highest priority. For example, support tasks or tasks with small periods may have higher priorities, because they need to run frequently.
Language Ref Manual references: 9.1 Abort Statements, 9.3 Task Execution - Task Activation, 9.5 Entries, Entry Calls, and Accept Statements, 9.8 Priorities, B Predefined Language Pragmas
Periodic:
loop
delay Interval;
...
end loop Periodic; |
No_Drift:
declare
use Calendar;
-- Interval is a global constant of type Duration
Next_Time : Calendar.Time := Calendar.Clock + Interval;
begin -- No_Drift
Stable_Periodic:
loop
delay Next_Time - Clock;
...
Next_Time := Next_Time + Interval;
end loop Stable_Periodic;
end No_Drift; |
delay statement is that the task is not scheduled for
execution before the interval has expired. In other words, a task becomes
eligible to resume execution as soon as the amount of time has passed.
However, there is no guarantee of when (or if) it is scheduled after that
time.A busy wait can interfere with processing by other tasks. It can consume the very processor resource necessary for completion of the activity for which it is waiting. Even a loop with a delay can have the impact of busy waiting if the planned wait is significantly longer then the delay interval. If a task has nothing to do, it should be blocked at an accept or select statement.
Using knowledge of the execution pattern of tasks to achieve timing requirements is nonportable. Ada does not specify the underlying scheduling algorithm.
Language Ref Manual references: 9.3 Task Execution - Task Activation, 9.6 Delay Statements, Duration, and Time, 9.7.3 Timed Entry Calls, C Predefined Language Environment