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

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

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):

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

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

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

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

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

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

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

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

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

communication

termination