Ada 95 Quality and Style Guide Chapter 6
Concurrency exists as either apparent concurrency or real concurrency.
In a single processor environment, apparent concurrency is the
result of interleaved execution of concurrent activities. In a
multiprocessor environment, real concurrency is the result of
overlapped execution of concurrent activities.
Concurrent programming is more difficult and error prone than
sequential programming. The concurrent programming features of
Ada are designed to make it easier to write and maintain concurrent
programs that behave consistently and predictably and avoid such
problems as deadlock and starvation. The language features themselves
cannot guarantee that programs have these desirable properties.
They must be used with discipline and care, a process supported
by the guidelines in this chapter.
The correct usage of Ada concurrency features results in reliable,
reusable, and portable software. Protected objects (added in Ada
95) encapsulate and provide synchronized access to their private
data (Rationale 1995, §II.9). Protected objects help you
manage shared data without incurring a performance penalty. Tasks
model concurrent activities and use the rendezvous to synchronize
between cooperating concurrent tasks. Much of the synchronization
required between tasks involves data synchronization, which can
be accomplished most efficiently, in general, using protected
objects. Misuse of language features results in software that
is unverifiable and difficult to reuse or port. For example,
using task priorities or delays to manage synchronization is not
portable. It is also important that a reusable component not
make assumptions about the order or speed of task execution (i.e.,
about the compiler's tasking implementation).
Although concurrent features such as tasks and protected objects
are supported by the core Ada language, care should be taken when
using these features with implementations that do not specifically
support Annex D
Guidelines in this chapter are frequently worded "consider
. . ." because hard and fast rules cannot apply in all situations.
The specific choice you make in a given situation involves design
tradeoffs. The rationale for these guidelines is intended to
give you insight into some of these tradeoffs.
Many problems map naturally to a concurrent programming solution.
By understanding and correctly using the Ada language concurrency
features, you can produce solutions that are largely independent
of target implementation. Tasks provide a means, within the Ada
language, of expressing concurrent, asynchronous threads of control
and relieving programmers from the problem of explicitly controlling
multiple concurrent activities. Protected objects serve as a building
block to support other synchronization paradigms.
Tasks cooperate to perform the required activities of the software.
Synchronization and mutual exclusion are required between individual
tasks. The Ada rendezvous and protected objects provide powerful
mechanisms for both synchronization and mutual exclusion.
6.1.1 Protected Objects
guideline
rationale
guideline
Multiple tasks that implement the decomposition of a large, matrix
multiplication algorithm are an example of an opportunity for
real concurrency in a multiprocessor target environment. In a
single processor target environment, this approach may not be
justified due to the overhead incurred from context switching
and the sharing of system resources.
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.
You should use tasks for separate threads of control. When you
synchronize tasks, you should use the rendezvous mechanism only
when you are trying to synchronize actual processes (e.g., specify
a time-sensitive ordering relationship or tightly coupled interprocess
communication). For most synchronization needs,
however, you should use protected objects (see Guideline 6.1.1),
which are more flexible and can minimize unnecessary bottlenecks.
Additionally, passive tasks are probably better modeled through
protected objects than active tasks.
Resources shared between multiple tasks, such as devices, require
control and synchronization because their operations are not atomic.
Drawing a circle on a display might 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.
guideline
The following example shows how to use discriminants to associate
data with tasks, thus allowing the tasks to be parameterized when
they are declared and eliminating the need for an initial rendezvous
with the task:
The next example shows how an initial rendezvous can
be used to associate data with tasks. This is more complicated
and more error prone than the previous example. This method is
no longer needed in Ada 95 due to the availability of discriminants
with task types and protected types:
Task discriminants provide a way for you to identify or parameterize
a task without the overhead of an initial rendezvous. For example,
you can use this discriminant to initialize a task or tell it
who it is (from among an array of tasks) (Rationale 1995, §II.9).
More importantly, you can associate the discriminant with specific
data. When you use an access discriminant, you can bind the data
securely to the task because the access discriminant is constant
and cannot be detached from the task (Rationale
1995, §9.6). This reduces and might eliminate bottlenecks
in the parallel activation of tasks (Rationale 1995, §9.6).
guideline
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.
The consistent and logical use of task and protected types, when
necessary, contributes to understandability. Identical tasks can
be declared using a common task type. Identical protected objects
can be declared using a common protected type. Dynamically allocated
task or protected structures are necessary when you must create
and destroy tasks or protected objects dynamically or when you
must reference them by different names.
guideline
You can use dynamically allocated tasks and protected objects
when you need to allow the number of tasks and protected objects
to vary during execution. When you must ensure that tasks are
activated in a particular order, you should use dynamically allocated
tasks because the Ada language does not define an activation order
for statically allocated task objects. In using dynamically allocated
tasks and protected objects, you face the same issues as with
any use of the heap.
guideline
At some point in its execution, T1 is blocked. Otherwise,
T2 and Server might 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:
You might expect that, due to its higher priority, 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 (unless pragma Queueing_Policy
is used). 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. (Annex D can change the
default policy of FIFO queues.)
A solution might be to provide an entry for a High priority
user and an entry for a Medium priority user.
However, in this approach, 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 inversion
occurs when lower priority tasks are given service while higher
priority tasks remain blocked. In
the first 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 because 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.
You should not rely upon task priorities to achieve an exact sequence
of execution or rely upon
them to achieve mutual exclusion. Although
the underlying dispatching model is common to all Ada 95 implementations,
there might be differences in dispatching, queuing, and locking
policies for tasks and protected objects.
All of these factors might lead to different sequences of execution.
If you need to ensure a sequence of execution, you should make
use of Ada's synchronization mechanisms, i.e., protected objects
or rendezvous.
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
might have higher priorities because they need to run frequently.
All production-quality validated Ada 95 compilers will probably
support pragma Priority. However, you should use caution
unless Annex D is specifically supported.
There is currently no universal consensus on how to apply the
basic principles of rate monotonic scheduling
(RMS) to the Ada 95 concurrency model. One basic principle of
RMS is to arrange all periodic tasks so that tasks with shorter
periods have higher priorities than tasks with longer periods.
However, with Ada 95, it might be faster to raise the priorities
of tasks whose jobs suddenly become critical than to wait for
an executive task to reschedule them. In this case, priority
inversion can be minimized using a protected object with pragma
Locking_Policy(Ceiling_Locking) as the server instead
of a task.
guideline
To avoid an inaccurate delay drift, you should use the delay
until statement. The following example (Rationale 1995, §9.3)
shows how to satisfy a periodic requirement with an average period:
The Ada language definition only guarantees that the delay time
is a minimum. The meaning of a delay or delay until
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 because the required
resources for that task might not be available at the expiration
of the delay.
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, an
entry call, or an appropriate delay.
The expiration time for a relative delay is rounded up to the
nearest clock tick. If you use the real-time clock features provided
by Annex D, however, clock ticks are guaranteed to be no greater
than one millisecond (Ada Reference Manual 1995, §D.8).
guideline
The reusability of common protected operations (e.g., mutually
exclusive read/write operations) can be maximized by using generic
implementations of abstract data types. These generic implementations
then provide templates that can be instantiated with data types
specific to individual applications.
The need for tasks to communicate gives rise to most of the problems
that make concurrent programming so difficult. Used properly,
Ada's intertask communication features can improve the reliability
of concurrent programs; used thoughtlessly, they can introduce
subtle errors that can be difficult to detect and correct.
6.2.1 Efficient Task Communication
guideline
When work is removed from the accept body and placed
later in the selective accept loop, the additional work
might still suspend the caller task. If the caller task calls
entry Operation again before the server task completes
its additional work, the caller is delayed until the server completes
the additional work. If the potential delay is unacceptable and
the additional work does not need to be completed before the next
service of the caller task, the additional work can form the basis
of a new task that will not block the caller task.
Operations on protected objects incur less execution overhead
than tasks and are more efficient for data synchronization and
communication than the rendezvous. You must design protected operations
to be bounded, short, and not potentially blocking.
Minimizing the work performed during a rendezvous or selective
accept loop of a task can increase the rate of execution only
when it results in additional overlaps in processing between the
caller and callee or when other tasks can be scheduled due to
the shorter period of execution. Therefore, the largest increases
in execution rates will be seen in multiprocessor environments.
In single-processor environments, the increased execution rate
will not be as significant and there might even be a small net
loss. The guideline is still applicable, however, if the application
could ever be ported to a multiprocessor environment.
guideline
In this select statement, if all the guards happen to
be closed, the program can continue by executing the else
part. There is no need for a handler for Program_Error.
Other exceptions can still be raised while evaluating the guards
or attempting to communicate. You will also need to include an
exception handler in the task Throttle so that it can
continue to execute after an exception is raised during the rendezvous:
In this select statement, if all the guards happen to
be closed, exception Program_Error will be raised. Other
exceptions can still be raised while evaluating the guards or
attempting to communicate:
Because an else part cannot have a guard, it can never
be closed off as an alternative action; thus, its presence prevents
Program_Error. However, an else part, a delay
alternative, and a terminate alternative are all mutually
exclusive, so you will not always be able to provide an else
part. In these cases, you must be prepared to handle Program_Error.
The exception Tasking_Error can
be raised in the calling task whenever it attempts to communicate.
There are many situations permitting this. Few of them are preventable
by the calling task.
If an exception is raised during a rendezvous and
not handled in the accept statement, it is propagated
to both tasks and must be handled in two places (see Guideline
5.8).
The handling of the others exception can be used to avoid
propagating unexpected exceptions to callers (when this is the
desired effect) and to localize the logic for dealing with unexpected
exceptions in the rendezvous. After handling, an unknown exception
should normally be raised again because the final decision of
how to deal with it might need to be made at the outermost scope
of the task body.
(Real-Time Systems). If Annex D is not specifically supported,
features required for real-time applications might not be implemented.
6.1 CONCURRENCY OPTIONS
example
example
-- The following example of a stock exchange simulation shows how naturally
-- concurrent objects within the problem domain can be modeled as Ada tasks.
-------------------------------------------------------------------------
-- Protected objects are used for the Display and for the Transaction_Queue
-- because they only need a mutual exclusion mechanism.
protected Display is
entry Shift_Tape_Left;
entry Put_Character_On_Tape (C : in Character);
end Display;
protected Transaction_Queue is
entry Put (T : in Transaction);
entry Get (T : out Transaction);
function Is_Empty return Boolean;
end Transaction_Queue;
-------------------------------------------------------------------------
-- A task is needed for the Ticker_Tape because it has independent cyclic
-- activity. The Specialist and the Investor are best modeled with tasks
-- since they perform different actions simultaneously, and should be
-- asynchronous threads of control.
task Ticker_Tape;
task Specialist is
entry Buy (Order : in Order_Type);
entry Sell (Order : in Order_Type);
end Specialist;
task Investor;
-------------------------------------------------------------------------
task body Ticker_Tape is
...
begin
loop
Display.Shift_Tape_Left;
if not More_To_Send (Current_Tape_String) and then
not Transaction_Queue.Is_Empty
then
Transaction_Queue.Get (Current_Tape_Transaction);
... -- convert Transaction to string
end if;
if More_To_Send (Current_Tape_String) then
Display.Put_Character_On_Tape (Next_Char);
end if;
delay until Time_To_Shift_Tape;
Time_To_Shift_Tape := Time_To_Shift_Tape + Shift_Interval;
end loop;
end Ticker_Tape;
task body Specialist is
...
loop
select
accept Buy (Order : in Order_Type) do
...
end Buy;
...
or
accept Sell (Order : in Order_Type) do
...
end Sell;
...
else
-- match orders
...
Transaction_Queue.Put (New_Transaction);
...
end select;
end loop;
end Specialist;
task body Investor is
...
begin
loop
-- some algorithm that determines whether the investor
-- buys or sells, quantity, price, etc
...
if ... then
Specialist.Buy (Order);
end if;
if ... then
Specialist.Sell (Order);
end if;
end loop;
end Investor;
example
(Rationale 1995, §9.1).
type Task_Data is
record
... -- data for task to work on
end record;
task type Worker (D : access Task_Data) is
...
end;
-- When you declare a task object of type Worker, you explicitly associate this task with
-- its data through the discriminant D
Data_for_Worker_X : aliased Task_Data := ...;
X : Worker (Data_for_Worker_X'Access);
task type Producer (Channel : Channel_Number; ID : ID_Number);
task body Producer is
begin
loop
... -- generate an item
Buffer.Put (New_Item);
end loop;
end Producer;
...
Keyboard : Producer (Channel => Keyboard_Channel, ID => 1);
Mouse : Producer (Channel => Mouse_Channel, ID => 2);
task type Producer is
entry Initialize (Channel : in Channel_Number; ID : in ID_Number);
end Producer;
task body Producer is
IO_Channel : Channel_Number;
Producer_ID : ID_Number;
begin
accept Initialize (Channel : in Channel_Number; ID : in ID_Number) do
IO_Channel := Channel;
Producer_ID := ID;
end;
loop
... -- generate an item
Buffer.Put (New_Item);
end loop;
end Producer;
...
Keyboard : Producer;
Mouse : Producer;
...
begin
...
Keyboard.Initialize (Channel => Keyboard_Channel, ID => 1);
Mouse.Initialize (Channel => Mouse_Channel, ID => 2);
...
example
task Buffer;
task type Buffer_Manager;
Channel : Buffer_Manager;
example
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
...
-- Radar_Track tasks created in previous passes through the loop
-- cannot be accessed from Current_Track after it is updated.
-- 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;
example
task T1 is
pragma Priority (High);
end T1;
task T2 is
pragma Priority (Medium);
end T2;
task Server is
entry Operation (...);
end Server;
----------------------------
task body T1 is
begin
...
Server.Operation (...);
...
end T1;
task body T2 is
begin
...
Server.Operation (...);
...
end T2;
task body Server is
begin
...
accept Operation (...);
...
end Server;
T1 blocks
T2 calls Server.Operation
T1 unblocks
T1 calls Server.Operation
-- Does Server accept the call from T1 or from T2?
---------------------------------------------------------------------
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;
---------------------------------------------------------------------
example
Periodic:
loop
delay Interval;
...
end loop Periodic;
task body Poll_Device is
use type Ada.Real_Time.Time;
use type Ada.Real_Time.Time_Span;
Poll_Time : Ada.Real_Time.Time := ...; -- time to start polling
Period : constant Ada.Real_Time.Time_Span := Ada.Real_Time.Milliseconds (10);
begin
loop
delay until Poll_Time;
... -- Poll the device
Poll_Time := Poll_Time + Period;
end loop;
end Poll_Device;
rationale
6.2 COMMUNICATION
example
...
loop
select
accept Operation do
-- These statements are executed during rendezvous.
-- Both caller and server are blocked during this time.
...
end Operation;
...
-- These statements are not executed during rendezvous.
-- The execution of these statements increases the time required
-- to get back to the accept and might be a candidate for another task.
or
accept Operation_2 do
-- These statements are executed during rendezvous.
-- Both caller and server are blocked during this time.
...
end Operation_2;
end select;
-- These statements are also not executed during rendezvous,
-- The execution of these statements increases the time required
-- to get back to the accept and might be a candidate for another task.
end loop;
example
Accelerate:
begin
Throttle.Increase(Step);
exception
when Tasking_Error => ...
when Constraint_Error => ...
when Throttle_Too_Wide => ...
...
end Accelerate;
...
Guarded:
begin
select
when Condition_1 =>
accept Entry_1;
or
when Condition_2 =>
accept Entry_2;
else -- all alternatives closed
...
end select;
exception
when Constraint_Error =>
...
end Guarded;
Guarded:
begin
select
when Condition_1 =>
accept Entry_1;
or
when Condition_2 =>
delay Fraction_Of_A_Second;
end select;
exception
when Program_Error => ...
when Constraint_Error => ...
end Guarded;
...
Chapter 6 continued on next page.
In This Guide:
Table of Contents
Chapter 1
Chapter 2
Chapter 3
Chapter 4
Chapter 5
Chapter 6
Chapter 7
Chapter 8
Chapter 9
Chapter 10
Chapter 11
Appendix
References
Bibliography
Index