Ada 95 Quality and Style Guide                            Chapter 6

Back to Sections 6.0 through 6.2.2.

6.2.3 Attributes 'Count, 'Callable, and 'Terminated

guideline

  • Do not depend on the values of the task attributes 'Callable or 'Terminated ( Nissen and Wallis 1984).
  • Do not depend on attributes to avoid Tasking_Error on an entry call.
  • For tasks, do not depend on the value of the entry attribute 'Count.
  • Using the 'Count attribute with protected entries is more reliable than using the 'Count attribute with task entries.

    example

    In the following examples, Dispatch'Callable is a Boolean expression, indicating whether a call can be made to the task Intercept without raising the exception Tasking_Error. Dispatch'Count indicates the number of callers currently waiting at entry Transmit. Dispatch'Terminated is a Boolean expression, indicating whether the task Dispatch is in a 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 Dispatch is
    
    ...
    
       select
    
          when Transmit'Count > 0 and Receive'Count = 0 =>
             accept Transmit;
    
             ...
    
       or
    
          accept Receive;
    
          ...
    
       end select;
    
    ...
    
    end Dispatch;
    
    ---------------------------------------------------------------------
    
    

    If the following code is preempted between evaluating the condition and initiating the call, the assumption that the task is still callable might no longer be valid:

    ...
    
    if Dispatch'Callable then
    
       Dispatch.Receive;
    
    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 might 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 Reference Manual (1995, §9.9) 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 accept statement might result in the opening of alternatives that should not be opened under a changed value of 'Count.

    The value of the attribute 'Count is stable for protected units because any change to an entry queue is itself a protected action, which will not occur while any other protected action is already proceeding. Nevertheless, when you use 'Count within an entry barrier of a protected unit, you should remember that the condition of the barrier is evaluated both before and after queueing a given caller.

    6.2.4 Unprotected Shared Variables

    guideline

  • Use calls on protected subprograms or entries to pass data between tasks rather than unprotected shared variables.
  • Do not use unprotected shared variables as a task synchronization device.
  • Do not reference nonlocal variables in a guard .
  • If an unprotected shared variable is necessary, use the pragma Volatile or Atomic.

  • 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. This is because there is no synchronization or mutual exclusion between the task that reads a command and the one that acts on it. Without knowledge about their relative scheduling, the actual results cannot be predicted:

    -----------------------------------------------------------------------
    
    task body Line_Printer_Driver is
    
       ...
    
    begin
    
       loop
    
          Current_Line := Line_Buffer;
    
          -- send to device
    
       end loop;
    
    end Line_Printer_Driver;
    
    -----------------------------------------------------------------------
    
    task body Spool_Server is
    
       ...
    
    begin
    
       loop
    
          Disk_Read (Spool_File, Line_Buffer);
    
       end loop;
    
    end Spool_Server;
    
    -----------------------------------------------------------------------
    
    

    The following example shows a vending machine that dispenses the amount requested into an appropriately sized container. The guards reference the global variables Num_Requested and Item_Count, leading to a potential problem in the wrong amount being dispensed into an inappropriately sized container:

    Num_Requested : Natural;
    
    Item_Count    : Natural := 1000;
    
    task type Request_Manager (Personal_Limit : Natural := 1) is
    
       entry Make_Request (Num : Natural);
    
       entry Get_Container;
    
       entry Dispense;
    
    end Request_Manager;
    
    
    
    task body Request_Manager is
    
    begin
    
       loop
    
          select
    
             accept Make_Request (Num : Natural) do
    
                Num_Requested := Num;
    
             end Make_Request;
    
          or
    
             when Num_Requested < Item_Count =>
                accept Get_Container;
    
                ...
    
          or
    
             when Num_Requested < Item_Count =>
                accept Dispense do
    
                   if Num_Requested <= Personal_Limit then
    
                      Ada.Text_IO.Put_Line ("Please pick up items.");
    
                   else
    
                      Ada.Text_IO.Put_Line ("Sorry! Requesting too many items.");
    
                   end if;
    
                end Dispense;
    
          end select;
    
       end loop;
    
    end Request_Manager;
    
    R1 : Request_Manager (Personal_Limit => 10);
    
    R2 : Request_Manager (Personal_Limit => 2);  
    
    

    The interleaving of the execution of R1 and R2 can lead to Num_Requested being changed before the entry call to Dispense is accepted. Thus, R1 might receive fewer items than requested or R2's request might be bounced because the request manager thinks that what R2 is requesting exceeds R2's personal limit. By using the local variable, you will dispense the correct amount. Furthermore, by using the pragma Volatile (Ada Reference Manual 1995, §C.6), you ensure that the Item_Count is reevaluated when the guards are evaluated. Given that the variable Item_Count is not updated in this task body, the compiler might otherwise have optimized the code and not generated code to reevaluate Item_Count every time it is read:

    Item_Count : Natural := 1000;
    
    pragma Volatile (Item_Count);
    
    task body Request_Manager is
    
       Local_Num_Requested : Natural := 0;
    
    begin
    
       loop
    
          select
    
             accept Make_Request (Num : Natural) do
    
                Local_Num_Requested := Num;
    
             end Make_Request;
    
          or
    
             when Local_Num_Requested <= Personal_Limit =>
                accept Get_Container;
    
                ...
    
          or
    
             when Local_Num_Requested < Item_Count =>
                accept Dispense do
    
                   ... -- output appropriate message if couldn't service request
    
                end Dispense;
    
                Item_Count := Item_Count - Local_Num_Requested; 
    
          end select;
    
       end loop;
    
    end Request_Manager;
    
    

    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 unprotected data correctly. If it is not done correctly, the reliability of the program suffers.

    Ada provides protected objects that encapsulate and provide synchronized access to protected data that is shared between tasks. Protected objects are expected to provide better performance than the rendezvous that usually requires introduction of an additional task to manage the shared data. The use of unprotected shared variables is more error-prone than the protected objects or rendezvous because the programmer must ensure that the unprotected shared variables are independently addressable and that the actions of reading or updating the same unprotected shared variable are sequential (Ada Reference Manual 1995, §9.10; Rationale 1995, §II.9).

    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.

    You might need to use an object in shared memory to communicate data between (Rationale 1995, §C.5):

    - Ada tasks
    - An Ada program and concurrent non-Ada processes
    - An Ada program and hardware devices

    If your environment supports the Systems Programming Annex (Ada Reference Manual 1995, Annex C), you should indicate whether loads and stores to the shared object must be indivisible. If you specify the pragma Atomic, make sure that the object meets the underlying hardware requirements for size and alignment.

    Multiple tasks sharing the predefined random number generator and certain input/output subprograms can lead to problems with unprotected updates to shared state. The Ada Reference Manual (1995, §A.5.2) points out the need for tasks to synchronize their access to the random number generators (packages Ada.Numerics.Float_Random and Ada.Numerics.Discrete_Random). See Guideline 7.7.5 for the I/O issue.

    6.2.5 Selective Accepts and Entry Calls

    guideline

  • Use caution with conditional entry calls to task entries.
  • Use caution with selective accept with else parts.
  • Do not depend upon a particular delay in timed entry calls to task entries.
  • Do not depend upon a particular delay in selective accepts with delay alternatives.
  • Consider using protected objects instead of the rendezvous for data-oriented synchronization.

  • example

    The conditional entry call in the following code results in a potential race condition that might degenerate into a busy waiting loop (i.e., perform the same calculation over and over). The task Current_Position containing entry Request_New_Coordinates might never execute if the loop-containing task (shown in the following code fragment) has a higher priority than Current_Position because it does not release the processing resource:

    task body Calculate_Flightpath is
    
    begin
    
       ...
    
       loop
    
      
    
          select
    
             Current_Position.Request_New_Coordinates (X, Y);
    
             -- calculate projected location based on new coordinates
    
             ...
    
      
    
          else
    
             -- calculate projected location based on last locations
    
             ...
    
          end select;
    
      
    
       end loop;
    
       ...
    
    end Calculate_Flightpath;
    
    

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

    task body Calculate_Flightpath is
    
    begin
    
       ...
    
       loop
    
      
    
          select
    
             Current_Position.Request_New_Coordinates(X, Y);
    
             -- calculate projected location based on new coordinates
    
             ...
    
      
    
          else
    
             -- calculate projected location based on last locations
    
             ...
    
      
    
             delay until Time_To_Execute;
    
             Time_To_Execute := Time_To_Execute + Period;
    
          end select;
    
      
    
       end loop;
    
       ...
    
    end Calculate_Flightpath;
    
    
    
    

    The following selective accept with else again does not degenerate into a busy wait loop only because of the addition of a delay statement:

    task body Buffer_Messages is
    
    
    
       ...
    
    
    
    begin
    
    
    
       ...
    
    
    
       loop
    
          delay until Time_To_Execute;
    
    
    
          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;
    
    
    
          Time_To_Execute := Time_To_Execute + Period;
    
       end loop;
    
    
    
       ...
    
    
    
    end Buffer_Messages;
    
    
    
    

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

    task body Monitor_Reactor is
    
       ...
    
    begin
    
       ...
    
       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;
    
       ...
    
    end Monitor_Reactor;
    
    
    
    

    In the following "selective accept 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:

    task body Current_Position is
    
    begin
    
       ...
    
       loop
    
      
    
          select
    
             accept Request_New_Coordinates (X :    out Integer;
    
                                             Y :    out Integer) do
    
                -- copy coordinates to parameters
    
                ...
    
             end Request_New_Coordinates;
    
      
    
          or
    
             delay until Time_To_Execute;
    
          end select;
    
      
    
          Time_To_Execute := Time_To_Execute + Period;
    
          -- Read Sensors
    
          -- execute coordinate transformations
    
       end loop;
    
       ...
    
    end Current_Position;
    
    
    
    

    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 accepts with else parts, the Ada Reference Manual (1995, §9.7) does not define "immediately." For timed entry calls and selective accepts with delay alternatives, implementors might 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.7).

    Protected objects offer an efficient means for providing data-oriented synchronization. Operations on protected objects incur less execution overhead than tasks and are more efficient for data synchronization and communication than the rendezvous. See Guideline 6.1.1 for an example of this use of protected objects.

    6.2.6 Communication Complexity

    guideline

  • Minimize the number of accept and select statements per task .
  • Minimize the number of accept statements per entry.

  • 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).

    notes

    Sanden (1994) argues that you need to trade off the complexity of the guards associated with the accept statements against the number of select/accept statements. Sanden (1994) shows an example of a queue controller for bank tellers where there are two modes, open and closed. You can implement this scenario with one loop and two select statements, one for the open mode and the other for the closed mode. Although you are using more select/accept statements, Sanden (1994) argues that the resulting program is easier to understand and verify.

  • 6.3 TERMINATION

    The ability of tasks to interact with each other using Ada's intertask communication features makes it especially important to manage planned or unplanned (e.g., in response to a catastrophic exception condition) termination in a disciplined way. To do otherwise can lead to a proliferation of undesired and unpredictable side effects as a result of the termination of a single task.

    The guidelines on termination focus on the termination of tasks. Wherever possible, you should use protected objects (see Guideline 6.1.1), thus avoiding the termination problems associated with tasks.

    6.3.1 Avoiding Undesired Termination

    guideline

  • Consider using an exception handler for a rendezvous within the main loop inside each task.

  • example

    In the following example, an exception raised using the primary sensor is used to change Mode to Degraded still allowing execution of the system:

    ...
    
    loop
    
    
    
       Recognize_Degraded_Mode:
    
          begin
    
    
    
             case Mode is
    
                when Primary =>
                   select
    
                      Current_Position_Primary.Request_New_Coordinates (X, Y);
    
                   or
    
                      delay 0.25;
    
                      -- Decide whether to switch modes;
    
                   end select;
    
    
    
                when Degraded =>
    
    
                   Current_Position_Backup.Request_New_Coordinates (X, Y);
    
    
    
             end case;
    
    
    
             ...
    
          exception
    
             when Tasking_Error | Program_Error =>
                Mode := Degraded;
    
          end Recognize_Degraded_Mode;
    
    
    
    end loop;
    
    ...
    
    
    
    

    rationale

    Allowing a task to terminate might not support the requirements of the system. Without an exception handler for the rendezvous within the main task loop, the functions of the task might not be performed.

    notes

    The use of an exception handler is the only way to guarantee recovery from an entry call to an abnormal task. Use of the 'Terminated attribute to test a task's availability before making the entry call can introduce a race condition where the tested task fails after the test but before the entry call (see Guideline 6.2.3).

    6.3.2 Normal Termination

    guideline

  • Do not create nonterminating tasks unintentionally.
  • Explicitly shut down tasks that depend on library packages.
  • Confirm that a task is terminated before freeing it with Ada.Unchecked_Deallocation.
  • Consider using a select statement with a terminate alternative rather than an accept statement alone.
  • Consider providing a terminate alternative for every selective accept that does not require an else part or a delay .
  • Do not declare or create a task within a user-defined Finalize procedure after the environment task has finished waiting for other tasks.

  • example

    This task will never terminate:

    ---------------------------------------------------------------------
    
    task body Message_Buffer is
    
       ...
    
    begin  -- Message_Buffer
    
       loop
    
          select
    
             when Head /= Tail => -- Circular buffer not empty
    
                accept Retrieve (Value :    out Element) do
    
                   ...
    
                end Retrieve;
    
                  
    
          or
    
             when not ((Head  = Index'First and then
    
                        Tail  = Index'Last) or else
    
                       (Head /= Index'First and then
    
                        Tail  = Index'Pred(Head))    )
    
                     => -- Circular buffer not full
    
                accept Store (Value : in     Element);
    
          end select;
    
       end loop;
    
    ...
    
    end Message_Buffer;
    
    ---------------------------------------------------------------------
    
    

    rationale

    The implicit environment task does not terminate until all other tasks have terminated. The environment task serves as a master for all other tasks created as part of the execution of the partition; it awaits termination of all such tasks in order to perform finalization of any remaining objects of the partition. Thus, a partition will exist until all library tasks are terminated.

    A nonterminating task is a task whose body consists of a nonterminating loop with no selective accept with terminate or a task that depends on a library package. Execution of a subprogram or block containing a task cannot complete until the task terminates. Any task that calls a subprogram containing a nonterminating task will be delayed indefinitely.

    A task that depends on a library package cannot be forced to terminate using a selective accept construct with alternative and should be terminated explicitly during program shutdown. One way to explicitly shut down tasks that depend on library packages is to provide them with exit entries and have the main subprogram call the exit entry just before it terminates.

    The Ada Reference Manual (1995, §13.11.2) states that a bounded error results from freeing a discriminated, unterminated task object. The danger lies in deallocating the discriminants as a result of freeing the task object. The effect of unterminated tasks containing bounded errors at the end of program execution is undefined.

    Execution of an accept statement or of a selective accept statement without an else part, a delay, or a terminate alternative cannot proceed if no task ever calls the entry(s) associated with that statement. This could result in deadlock. Following the guideline to provide a terminate alternative for every selective accept without an else or a delay entails programming multiple termination points in the task body. A reader can easily "know where to look" for the normal termination points in a task body. The termination points are the end of the body's sequence of statements and alternatives to select statements.

    When the environment task has been terminated, either normally or abnormally, the language does not specify whether to await a task activated during finalization of the controlled objects in a partition. While the environment task is waiting for all other tasks in the partition to complete, starting up a new task during finalization results in a bounded error (Ada Reference Manual 1995, §10.2). The exception Program_Error can be raised during creation or activation of such a task.

    exceptions

    If you are implementing a cyclic executive, you might need a scheduling task that does not terminate. It has been said that no real-time system should be programmed to terminate. This is extreme. Systematic shutdown of many real-time systems is a desirable safety feature.

    If you are considering programming a task not to terminate, be certain that it is not a dependent of a block or subprogram from which the task's caller(s) will ever expect to return. Because entire programs can be candidates for reuse (see Chapter 8), note that the task (and whatever it depends upon) will not terminate. Also be certain that for any other task that you do wish to terminate, its termination does not await this task's termination. Reread and fully understand the Ada Reference Manual (1995, §9.3) on "Task Dependence-Termination of Tasks."

    6.3.3 The Abort Statement

    guideline

  • Avoid using the abort statement.
  • Consider using the asynchronous select statement rather than the abort statement.
  • Minimize uses of the asynchronous select statement.
  • Avoid assigning nonatomic global objects from a task or from the abortable part of an asynchronous select statement.

  • example

    If required in the application, provide a task entry for orderly shutdown.

    The following example of asynchronous transfer of control shows a database transaction. The database operation may be cancelled (through a special input key) unless the commit transaction has begun. The code is extracted from the Rationale (1995, §9.4):

    with Ada.Finalization;
    
    package Txn_Pkg is
    
       type Txn_Status is (Incomplete, Failed, Succeeded);
    
       type Transaction is new Ada.Finalization.Limited_Controlled with private;
    
       procedure Finalize (Txn : in out transaction);
    
       procedure Set_Status (Txn    : in out Transaction;
    
                             Status : in     Txn_Status);
    
    private
    
       type Transaction is new Ada.Finalization.Limited_Controlled with
    
          record
    
             Status : Txn_Status := Incomplete;
    
             pragma Atomic (Status);
    
             . . . -- More components
    
          end record;
    
    end Txn_Pkg;
    
    -----------------------------------------------------------------------------
    
    package body Txn_Pkg is
    
       procedure Finalize (Txn : in out Transaction) is
    
       begin
    
          -- Finalization runs with abort and ATC deferred
    
          if Txn.Status = Succeeded then
    
             Commit (Txn);
    
          else
    
             Rollback (Txn);
    
          end if;
    
       end Finalize;
    
       . . . -- body of procedure Set_Status
    
    end Txn_Pkg;
    
    ----------------------------------------------------------------------------
    
    -- sample code block showing how Txn_Pkg could be used:
    
    declare
    
       Database_Txn : Transaction;
    
       -- declare a transaction, will commit or abort during finalization
    
    begin
    
       select  -- wait for a cancel key from the input device
    
          Input_Device.Wait_For_Cancel;
    
          -- the Status remains Incomplete, so that the transaction will not commit
    
       then abort  -- do the transaction
    
          begin
    
             Read (Database_Txn, . . .);
    
             Write (Database_Txn, . . .);
    
             . . .
    
             Set_Status (Database_Txn, Succeeded);
    
             -- set status to ensure the transaction is committed
    
          exception
    
             when others =>
                Ada.Text_IO.Put_Line ("Operation failed with unhandled exception:");
    
                Set_Status (Database_Txn, Failed);
    
          end;
    
       end select;
    
       -- Finalize on Database_Txn will be called here and, based on the recorded
    
       -- status, will either commit or abort the transaction.
    
    end;
    
    

    rationale

    When an abort statement is executed, there is no way to know what the targeted task was doing beforehand. Data for which the target task is responsible might be left in an inconsistent state. The overall effect on the system of aborting a task in such an uncontrolled way requires careful analysis. The system design must ensure that all tasks depending on the aborted task can detect the termination and respond appropriately.

    Tasks are not aborted until they reach an abort completion point such as beginning or end of elaboration, a delay statement, an accept statement, an entry call, a select statement, task allocation, or the execution of an exception handler. Consequently, the abort statement might not release processor resources as soon as you might expect. It also might not stop a runaway task because the task might be executing an infinite loop containing no abort completion points. There is no guarantee that a task will not abort until an abort completion point in multiprocessor systems, but the task will almost always stop running right away.

    An asynchronous select statement allows an external event to cause a task to begin execution at a new point, without having to abort and restart the task (Rationale 1995, §9.3). Because the triggering statement and the abortable statement execute in parallel until one of them completes and forces the other to be abandoned, you need only one thread of control. The asynchronous select statement improves maintainability because the abortable statements are clearly delimited and the transfer cannot be mistakenly redirected.

    In task bodies and in the abortable part of an asynchronous select, you should avoid assigning to nonatomic global objects, primarily because of the risk of an abort occurring before the nonatomic assignment completes. If you have one or more abort statements in your application and the assignment is disrupted, the target object can become abnormal, and subsequent uses of the object lead to erroneous execution (Ada Reference Manual 1995, §9.8). In the case of scalar objects, you can use the attribute 'Valid, but there is no equivalent attribute for nonscalar objects. (See Guideline 5.9.1 for a discussion of the 'Valid attribute.) You also can still safely assign to local objects and call operations of global protected objects.

    6.3.4 Abnormal Termination

    guideline

  • Place an exception handler for others at the end of a task body.
  • Consider having each exception handler at the end of a task body report the task's demise.
  • Do not rely on the task status to determine whether a rendezvous can be made with the task.

  • example

    This is one of many tasks updating the positions of blips on a radar screen. When started, it is given part of the name by which its parent knows it. Should it terminate due to an exception, it signals the fact in one of its parent's data structures:

    task type Track (My_Index : Track_Index) is
    
       ...
    
    end Track;
    
    ---------------------------------------------------------------------
    
    task body Track is
    
         Neutral : Boolean := True;
    
    begin  -- Track
    
       select
    
          accept ...
    
          ...
    
       or
    
          terminate;
    
       end select;
    
       ...
    
    exception
    
       when others =>
          if not Neutral then
    
             Station(My_Index).Status := Dead;
    
          end if;
    
    end Track;
    
    ---------------------------------------------------------------------
    
    

    rationale

    A task will terminate if an exception is raised within it for which it has no handler. In such a case, the exception is not propagated outside of the task (unless it occurs during a rendezvous). The task simply dies with no notification to other tasks in the program. Therefore, providing exception handlers within the task, and especially a handler for others, ensures that a task can regain control after an exception occurs. If the task cannot proceed normally after handling an exception, this affords it the opportunity to shut itself down cleanly and to notify tasks responsible for error recovery necessitated by the abnormal termination of the task.

    You should not use the task status to determine whether a rendezvous can be made with the task. If Task A depends on Task B and Task A checks the status flag before it rendezvouses with Task B, there is a potential that Task B fails between the status test and the rendezvous. In this case, Task A must provide an exception handler to handle the Tasking_Error exception raised by the call to an entry of an abnormal task (see Guideline 6.3.1).

    6.3.5 Circular Task Calls

    guideline

  • Do not call a task entry that directly or indirectly results in a call to an entry of the original calling task.

  • rationale

    A software failure known as task deadlock will occur if a task calls one of its own entries directly or indirectly via a circular chain of calls.

    6.3.6 Setting Exit Status

    guideline

  • Avoid race conditions in setting an exit status code from the main program when using the procedure Ada.Command_Line.Set_Exit_Status.
  • In a program with multiple tasks, encapsulate, serialize, and check calls to the procedure Ada.Command_Line.Set_Exit_Status.

  • rationale

    In accordance with the rules of Ada, tasks in library-level packages may terminate after the main program task. If the program permits multiple tasks to use Set_Exit_Status, then there can be no guarantee that any particular status value is the one actually returned.

    6.4 SUMMARY

    concurrency options

  • Consider using protected objects to provide mutually exclusive access to data.
  • Consider using protected objects to control or synchronize access to data shared by multiple tasks .
  • Consider using protected objects to implement synchronization, such as a passive resource monitor.
  • Consider encapsulating protected objects in the private part or body of a package.
  • Consider using a protected procedure to implement an interrupt handler.
  • Do not attach a protected procedure handler to a hardware interrupt if that interrupt has a maximum priority greater than the ceiling priority assigned to the handler.
  • Avoid the use of global variables in entry barriers.
  • Avoid the use of barrier expressions with side effects.
  • Use tasks to model selected asynchronous threads of control within the problem domain.
  • Consider using tasks to define concurrent algorithms.
  • Consider using rendezvous when your application requires synchronous unbuffered communication.
  • Consider using discriminants to minimize the need for an explicit initialization operation (Rationale 1995, §9.1).
  • Consider using discriminants to control composite components of the protected objects, including setting the size of an entry family (Rationale 1995, §9.1).
  • Consider using a discriminant to set the priority of a protected object (Rationale 1995, §9.1).
  • Consider using a discriminant to identify an interrupt to a protected object (Rationale 1995, §9.1).
  • Consider declaring a task type with a discriminant to indicate (Rationale 1995, §9.6):

    - Priority, storage size, and size of entry families of individual tasks of a type
    - Data associated with a task (through an access discriminant)

  • Consider using single task declarations to declare unique instances of concurrent tasks.
  • Consider using single protected declarations to declare unique instances of protected objects.
  • Minimize dynamic creation of tasks because of the potentially high startup overhead; reuse tasks by having them wait for new work on some appropriate entry queue.
  • Do not rely on pragma Priority unless your compiler supports the Real-Time Annex (Ada Reference Manual 1995, Annex D) and priority scheduling.
  • Minimize risk of priority inversion by use of protected objects and ceiling priority.
  • Do not rely upon task priorities to achieve a particular sequence of task execution.
  • Do not depend on a particular delay being achievable (Nissen and Wallis 1984).
  • Use a delay until not a delay statement to delay until a specific time has been reached.
  • Avoid using a busy waiting loop instead of a delay.
  • Carefully consider the placement of components of protected types within a tagged type inheritance hierarchy.
  • Consider using generics to provide extensibility of data types requiring the restrictions provided by protected objects.

  • communication

  • Minimize the work performed during a rendezvous .
  • Minimize the work performed in the selective accept loop of a task.
  • Consider using protected objects for data synchronization and communication.
  • Provide a handler for exception Program_Error whenever you cannot avoid a selectiveaccept statement whose alternatives can all be closed (Honeywell 1986).
  • Make systematic use of handlers for Tasking_Error.
  • Be prepared to handle exceptions during a rendezvous .
  • Consider using a when others exception handler.
  • Do not depend on the values of the task attributes 'Callable or 'Terminated (Nissen and Wallis 1984).
  • Do not depend on attributes to avoid Tasking_Error on an entry call.
  • For tasks, do not depend on the value of the entry attribute 'Count.
  • Using the 'Count attribute with protected entries is more reliable than using the 'Count attribute with task entries.
  • Use calls on protected subprograms or entries to pass data between tasks rather than unprotected shared variables.
  • Do not use unprotected shared variables as a task synchronization device.
  • Do not reference nonlocal variables in a guard .
  • If an unprotected shared variable is necessary, use the pragma Volatile or Atomic.
  • Use caution with conditional entry calls to task entries.
  • Use caution with selective accepts with else parts.
  • Do not depend upon a particular delay in timed entry calls to task entries.
  • Do not depend upon a particular delay in selective accepts with delay alternatives.
  • Consider using protected objects instead of the rendezvous for data-oriented synchronization.
  • Minimize the number of accept and select statements per task .
  • Minimize the number of accept statements per entry.

  • termination

  • Consider using an exception handler for a rendezvous within the main loop inside each task.
  • Do not create nonterminating tasks unintentionally.
  • Explicitly shut down tasks that depend on library packages.
  • Confirm that a task is terminated before freeing it with Ada.Unchecked_Deallocation.
  • Consider using a select statement with a terminate alternative rather than an accept statement alone.
  • Consider providing a terminate alternative for every selective accept that does not require an else part or a delay.
  • Do not declare or create a task within a user-defined Finalize procedure after the environment task has finished waiting for other tasks.
  • Avoid using the abort statement.
  • Consider using the asynchronous select statement rather than the abort statement.
  • Minimize uses of the asynchronous select statement.
  • Avoid assigning nonatomic global objects from a task or from the abortable part of an asynchronous select statement.
  • Place an exception handler for others at the end of a task body.
  • Consider having each exception handler at the end of a task body report the task's demise.
  • Do not rely on the task status to determine whether a rendezvous can be made with the task.
  • Do not call a task entry that directly or indirectly results in a call to an entry of the original calling task.
  • Avoid race conditions in setting an exit status code from the main program when using the procedure Ada.Command_Line.Set_Exit_Status.
  • In a program with multiple tasks, encapsulate, serialize, and check calls to the procedure Ada.Command_Line.Set_Exit_Status.