[Ada Information Clearinghouse]

Ada '83 Quality and Style:

Guidelines for Professional Programmers

Copyright 1989, 1991,1992 Software Productivity Consortium, Inc., Herndon, Virginia.

CHAPTER 6: Concurrency

6.2 Communication

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.

Language Ref Manual references: 9.5 Entries, Entry Calls, and Accept Statements

In this section...
6.2.1 Efficient Task Communications
6.2.2 Defensive Task Communication
6.2.3 Attributes 'Count, 'Callable and 'Terminated
6.2.4 Shared Variables
6.2.5 Tentative Rendezvous Constructs
6.2.6 Communication Complexity
Summary of Guidelines from this section


6.2.1 Efficient Task Communications

guideline

example

In the following example, the statements in the accept body are performed as part of the execution of both the caller task and the task Server which contains Operation and Operation2. The statements after the accept body are executed before Server can accept additional calls to Operation or Operation2.
   ... 
   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. 
         -- Their execution delays getting back to the accept and 
         --   may 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, 
      -- Their execution delays getting back to the accept and may 
      --   be a candidate for another task.
      
   end loop;

rationale

Only work that needs to be performed during a rendezvous, such as saving or generating parameters, should be allowed in the accept bodies to minimize the time required to rendezvous.

When work is removed from the accept body and placed later in the selective wait loop, the additional work may 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 may form the basis of a new task that will not block the caller task.

note

In some cases, additional functions may be added to a task. For example, a task controlling a communication device may be responsible for a periodic function to ensure that the device is operating correctly. This type of addition should be done with care realizing that the response time of the task is impacted (see rationale).

exceptions

Task communication overhead must be balanced with the associated blocking. Each time a new task is introduced, there is a timing impact caused by scheduling and synchronization with the new task. Be careful when introducing tasks to reduce blocking. The reduction in blocking time will cause increased task scheduling and synchronization overhead and software architecture complexity.

Language Ref Manual references: 9.7 Select Statements, 9.7.1 Selective Waits


6.2.2 Defensive Task Communication

guideline

example

This block allows recovery from exceptions raised while attempting to communicate a command to another task.
Accelerate: 
   begin 
      Throttle.Increase(Step);
      
   exception 
      when Tasking_Error     =>     ... 
      when Constraint_Error | 
           Numeric_Error     =>     ... 
      when Throttle_Too_Wide =>     ...
      
      ... 
   end Accelerate;

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.
... 
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 | Numeric_Error => 
         ... 
   end Guarded;

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.
  
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 | 
           Numeric_Error     =>  ... 
   end Guarded; 
...

rationale

The exception Program_Error is raised if a selective wait statement (select statement containing accepts) is reached, all of whose alternatives are closed (i.e., the guards evaluate to False and there are no alternatives without guards), unless there is an else part. When all alternatives are closed, the task can never again progress, so there is by definition an error in its programming. You must be prepared to handle this error should it occur.

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

note

There are other ways to prevent Program_Error at a selective wait. These involve leaving at least one alternative unguarded, or proving that at least one guard will evaluate True under all circumstances. The point here is that you, or your successors, will make mistakes in trying to do this, so you should prepare to handle the inevitable exception.

Language Ref Manual references: 9.7.1 Selective Waits, 11.2 Exception Handlers, 11.4 Exception Handling, 11.5 Exceptions Raised During Task Communication


6.2.3 Attributes 'Count, 'Callable and 'Terminated

guideline

example

In the following examples Intercept'Callable is a boolean indicating if a call can be made to the task Intercept without raising the exception Tasking_Error. Launch'Count indicates the number of callers currently waiting at entry Launch. Intercept'Terminated is a boolean indicating if the task Intercept is in terminated state.

This task is badly programmed because it relies upon the values of the 'Count attributes not changing between evaluating and acting upon them.
--------------------------------------------------------------------- 
task body Intercept is 
...

   select 
      when Launch'Count > 0 and Recall'Count = 0 => 
         accept Launch; 
         ...
         
   or 
      accept Recall; 
      ... 
   end select;
   
... 
end Intercept; 
---------------------------------------------------------------------

If the following code is preempted between evaluating the condition and initiating the call, the assumption that the task is still callable may no longer be valid.
... 
if Intercept'Callable then 
   Intercept.Recall; 
end if; 
...

rationale

Attributes 'Callable, 'Terminated, and 'Count are all subject to race conditions. Between the time you reference an attribute and the time you take action the value of the attribute may change. Attributes 'Callable and 'Terminated convey reliable information once they become False and True, respectively. If 'Callable is False, you can expect the callable state to remain constant. If 'Terminated is True, you can expect the task to remain terminated. Otherwise, 'Terminated and 'Callable can change between the time your code tests them and the time it responds to the result.

The Ada Language Reference Manual (Department of Defense 1983) itself warns about the asynchronous increase and decrease of the value of 'Count. A task can be removed from an entry queue due to execution of an abort statement as well as expiration of a timed entry call. The use of this attribute in guards of a selective wait statement may result in the opening of alternatives which should not be opened under a changed value of 'Count.

Language Ref Manual references: 9.4 Task Dependence - Termination of Tasks, 9.9 Task and Entry Attributes, A Predefined Language Attributes


6.2.4 Shared Variables

guideline

example

This code will either print the same line more than once, fail to print some lines, or print garbled lines (part of one line followed by part of another) nondeterministically.
--------------------------------------------------------------------- 
task body Robot_Arm_Driver is

   Current_Command : Robot_Command;
   
begin  -- Robot_Arm_Driver 
   loop
   
      Current_Command := Command; 
      -- send to device
      
   end loop; 
... 
end Robot_Arm_Driver;

--------------------------------------------------------------------- 
task body Stream_Server is 
begin 
   loop
   
      Stream_Read(Stream_File, Command);
      
   end loop; 
... 
end Stream_Server; 
---------------------------------------------------------------------

This code ensures that a missile cannot be fired unless the doors are open and that the missile cannot be armed unless the doors are shut. In this case the requirement for arming may be derived from the duration that the door may be open (i.e., arm first, open door, launch, close door).
Doors_Open : Boolean := False;

--------------------------------------------------------------------- 
task body Intercept is 
begin 
   ...
   
   select 
      when Doors_Open = True => 
         accept Launch; 
         ...
         
   or 
      when Doors_Open = False => 
         accept Arm; 
         ... 
   end select;
   
... 
end Intercept; 
---------------------------------------------------------------------

--------------------------------------------------------------------- 
task body Intercept is

   Local_Doors_Open : Boolean := False;
   
begin  -- Intercept 
   ...
   
   select 
      when Local_Doors_Open = True => 
         accept Launch; 
         ...
         
   or 
      when Local_Doors_Open = False => 
         accept Arm; 
         ...
         
   or 
      accept Door_Status 
            (Doors_Open : in     Boolean) do 
         Local_Doors_Open := Doors_Open; 
      end Door_Status; 
   end select;
   
... 
end Intercept; 
---------------------------------------------------------------------

rationale

There are many techniques for protecting and synchronizing data access. You must program most of them yourself to use them. It is difficult to write a program that shares data correctly. If it is not done correctly, the reliability of the program suffers. Ada provides the rendezvous to support synchronization and communication of information between tasks. Data that you might be tempted to share can be put into a task body with read and write entries to access it.

The first example above has a race condition requiring perfect interleaving of execution. This code can be made more reliable by introducing a flag that is set by Spool_Server and reset by Line_Printer_Driver. An if (condition flag) then delay ... else would be added to each task loop in order to ensure that the interleaving is satisfied. However, notice that this approach requires a delay and the associated rescheduling. Presumably this rescheduling overhead is what is being avoided by not using the rendezvous.

A guard is a conditional select alternative starting with a when (see 9.7.1 in Department of Defense 1983). The second example above also has a race condition requiring two different things. First, the task that opens the doors must open the doors and update Doors_Open before allowing the intercept task to continue execution. Second, the run time system evaluation of the guard in the select statement cannot occur until the Doors_Open matches the next anticipated entry call. If the next call will be to ARM, then you must make sure that Doors_Open changes to False before the Intercept task reevaluates the select statement. If the select statement is evaluated while Doors_Open is True and Doors_Open is subsequently set to False, the select will continue to wait on the Launch until a Launch is received. An alternate approach is to use Local_Doors_Open in the example. This guarantees that the guards will be reevaluated upon a change in the value of Doors_Open.

exceptions

For some required synchronizations the rendezvous may not meet time constraints. Each case should be analyzed in detail to justify the use of pragma Shared, which presumably has less overhead than the rendezvous. Be careful to correctly implement a data access synchronization technique. Without great effort you might get it wrong. Pragma Shared can serve as an expedient against poor run time support systems. Do not always use this as an excuse to avoid the rendezvous because implementations are allowed to ignore pragma Shared (Nissen and Wallis 1984). When pragma Shared is implemented by compilers, the implementation is not always uniform and can still lead to nonportable code. Pragma Shared affects only those objects whose storage and retrieval are implemented as indivisible operations. Also, pragma Shared can only be used for variables of scalar or access type.

note

As pointed out above, a guarantee of noninterference may be difficult with implementations that ignore pragma Shared. If you must share data, share the absolute minimum amount necessary, and be especially careful. As always, encapsulate the synchronization portions of code.

The problem is with variables. Constants, such as tables fixed at compile time, may be safely shared between tasks.

Language Ref Manual references: 9.7.1 Selective Waits, 9.11 Shared Variables, B Predefined Language Pragmas


6.2.5 Tentative Rendezvous Constructs

guideline

example

The conditional entry call in the following code results in a race condition that may degenerate into a busy waiting loop. The task Current_Position containing entry Request_New_Coordinates may never execute if this task has a higher priority than Current_Position, because this task doesn't release the processing resource.
... 
loop

   select 
      Current_Position.Request_New_Coordinates(X, Y); 
      -- calculate target location based on new coordinates 
      ...
      
   else 
      -- calculate target location based on last locations 
      ... 
   end select;
   
end loop; 
...

The addition of a delay as shown may allow Current_Position to execute until it reaches an accept for Request_New_Coordinates.
... 
loop

   select 
      Current_Position.Request_New_Coordinates(X, Y); 
      -- calculate target location based on new coordinates 
      ...
      
   else 
      -- calculate target location based on last locations 
      ...
      
      delay Next_Execute - Clock; 
      Next_Execute := Next_Execute + Period; 
   end select;
   
end loop; 
...

The following selective wait with else again does not degenerate into a busy wait loop only because of the addition of a delay statement.
   loop 
      delay Next_Execute - Clock;
      
      select 
         accept Get_New_Message (Message : in     String) do 
            -- copy message to parameters 
            ... 
         end Get_New_Message;
         
      else  -- Don't wait for rendezvous 
         -- perform built in test Functions 
         ... 
      end select;
      
      Next_Execute := Next_Execute + Task_Period; 
   end loop;

The following timed entry call may be considered an unacceptable implementation if lost communications with the reactor for over 25 milliseconds results in a critical situation.
... 
loop

   select 
      Reactor.Status(OK);
      
   or 
      delay 0.025; 
      -- lost communication for more that 25 milliseconds 
      Emergency_Shutdown; 
   end select;
   
   -- process reactor status 
   ... 
end loop; 
...

In the following "selective wait with delay" example, the accuracy of the coordinate calculation function is bounded by time. For example, the required accuracy cannot be obtained unless Period is within + or - 0.005 seconds. This period cannot be guaranteed because of the inaccuracy of the delay statement.
... 
loop

   select 
      accept Request_New_Coordinates (X :    out Integer; 
                                      Y :    out Integer) do 
         -- copy coordinates to parameters 
         ... 
      end Request_New_Coordinates;
      
   or 
      delay Next_Execute - Calendar.Clock; 
   end select;
   
   Next_Execute := Next_Execute + Period; 
   -- Read Sensors 
   -- execute coordinate transformations 
end loop; 
...

rationale

Use of these constructs always poses a risk of race conditions. Using them in loops, particularly with poorly chosen task priorities, can have the effect of busy waiting.

These constructs are very much implementation dependent. For conditional entry calls and selective waits with else parts, the Ada Language Reference Manual (Department of Defense 1983) does not define "immediately." For timed entry calls and selective waits with delay alternatives, implementors may have ideas of time that differ from each other and from your own. Like the delay statement, the delay alternative on the select construct might wait longer than the time required (see Guideline 6.1.5).

Language Ref Manual references: 9.6 Delay Statements, Duration, and Time, 9.7.1 Selective Waits, 9.7.2 Conditional Entry Calls, 9.7.3 Timed Entry Calls


6.2.6 Communication Complexity

guideline

example

Use
accept A; 
if Mode_1 then 
   -- do one thing 
else  -- Mode_2 
   -- do something different 
end if;

rather than
if Mode_1 then 
   accept A; 
   -- do one thing
   
else  -- Mode_2 
   accept A; 
   -- do something different 
end if;

rationale

This guideline reduces conceptual complexity. Only entries necessary to understand externally observable task behavior should be introduced. If there are several different accept and select statements that do not modify task behavior in a way important to the user of the task, there is unnecessary complexity introduced by the proliferation of select/accept statements. Externally observable behavior important to the task user includes task timing behavior, task rendezvous initiated by the entry calls, prioritization of entries, or data updates (where data is shared between tasks).

Language Ref Manual references: 9.7 Select Statements


Back to document index