Ada Programming/Tasking

Tasks
A task unit is a program unit that is obeyed concurrently with the rest of an Ada program. The corresponding activity, a new locus of control, is called a task in Ada terminology, and is similar to a thread, for example in Java Threads. The execution of the main program is also a task, the anonymous environment task. A task unit has both a declaration and a body, which is mandatory. A task body may be compiled separately as a subunit, but a task may not be a library unit, nor may it be generic. Every task depends on a master, which is the immediately surrounding declarative region - a block, a subprogram, another task, or a package. The execution of a master does not complete until all its dependent tasks have terminated. The environment task is the master of all other tasks; it terminates only when all other tasks have terminated.

Task units are similar to packages in that a task declaration defines entities exported from the task, whereas its body contains local declarations and statements of the task.

A single task is declared as follows:

Single declarations of exported identifiers Single; ...    Single  local declarations and statements Single;

A task declaration can be simplified, if nothing is exported, thus:

No_Exports;

Ex. 1

Housekeeping Check_CPU; Backup_Disk; Check_CPU ...      Check_CPU; Backup_Disk ...      Backup_Disk; ;   Housekeeping;

It is possible to declare task types, thus allowing task units to be created dynamically, and incorporated in data structures:

T      ... T;  ... Task_1, Task_2 : T;  ... T      ... T;

Task types are limited, i.e. they are restricted in the same way as limited types, so assignment and comparison are not allowed.

Rendezvous
The only entities that a task may export are entries. An entry looks much like a procedure. It has an identifier and may have in, out or in out parameters. Ada supports communication from task to task by means of the entry call. Information passes between tasks through the actual parameters of the entry call. We can encapsulate data structures within tasks and operate on them by means of entry calls, in a way analogous to the use of packages for encapsulating variables. The main difference is that an entry is executed by the called task, not the calling task, which is suspended until the call completes. If the called task is not ready to service a call on an entry, the calling task waits in a (FIFO) queue associated with the entry. This interaction between calling task and called task is known as a rendezvous. The calling task requests rendezvous with a specific named task by calling one of its entries. A task accepts rendezvous with any caller of a specific entry by executing an accept statement for the entry. If no caller is waiting, it is held up. Thus entry call and accept statement behave symmetrically. (To be honest, optimized object code may reduce the number of context switches below the number implied by this poor description.)

There is, however, a big difference between a procedure and an entry. A procedure has exactly one body that is executed when called. There is no such relation between an entry and a corresponding accept statement. An entry may have more than one accept statement, and the code executed may be different each time. In fact, there even need not be an accept statement at all. (Calling such an entry leads to deadlock of the caller if not timed, of course.)

Ex. 2 The following task type implements a single-slot buffer, i.e. an encapsulated variable that can have values inserted and removed in strict alternation. Note that the buffer task has no need of state variables to implement the buffer protocol: the alternation of insertion and removal operations is directly enforced by the control structure in the body of Encapsulated_Buffer_Task_Type which is, as is typical, a loop.

Encapsulated_Buffer_Task_Type Insert (An_Item :  Item); Remove (An_Item : Item); Encapsulated_Buffer_Task_Type; ...  Buffer_Pool :  (0 .. 15)  Encapsulated_Buffer_Task_Type; This_Item  : Item; ...    Encapsulated_Buffer_Task_Type Datum : Item; Insert (An_Item :  Item) Datum := An_Item; Insert; Remove (An_Item : Item) An_Item := Datum; Remove; ;   Encapsulated_Buffer_Task_Type; ...  Buffer_Pool(1).Remove (This_Item); Buffer_Pool(2).Insert (This_Item);

Selective Wait
To avoid being held up when it could be doing productive work, a server task often needs the freedom to accept a call on any one of a number of alternative entries. It does this by means of the selective wait statement, which allows a task to wait for a call on any of two or more entries.

If only one of the alternatives in a selective wait statement has a pending entry call, then that one is accepted. If two or more alternatives have calls pending, the implementation is free to accept any one of them. For example, it could choose one at random. This introduces bounded non-determinism into the program. A sound Ada program should not depend on a particular method being used to choose between pending entry calls. (However, there are facilities to influence the method used, when that is necessary.)

Ex. 3

Encapsulated_Variable_Task_Type Store (An_Item :  Item); Fetch (An_Item : Item); Encapsulated_Variable_Task_Type; ...    Encapsulated_Variable_Task_Type Datum : Item; Store (An_Item : Item) Datum := An_Item; Store; Store (An_Item : Item) Datum := An_Item; Store; Fetch (An_Item : Item) An_Item := Datum; Fetch; ;      ;    Encapsulated_Variable_Task_Type;

x, y : Encapsulated_Variable_Task_Type;

creates two variables of type Encapsulated_Variable_Task_Type. They can be used thus:

it : Item; ...  x.Store(Some_Expression); ...  x.Fetch (it); y.Store (it);

Again, note that the control structure of the body ensures that an Encapsulated_Variable_Task_Type must be given an initial value by a first Store operation before any Fetch operation can be accepted.

Guards
Depending on circumstances, a server task may not be able to accept calls for some of the entries that have accept alternatives in a selective wait statement. The acceptance of any alternative can be made conditional by using a guard, which is Boolean precondition for acceptance. This makes it easy to write monitor-like server tasks, with no need for an explicit signaling mechanism, nor for mutual exclusion. An alternative with a True guard is said to be open. It is an error if no alternative is open when the selective wait statement is executed, and this raises the Program_Error exception.

Ex. 4

Cyclic_Buffer_Task_Type Insert (An_Item :  Item); Remove (An_Item : Item); Cyclic_Buffer_Task_Type; ...    Cyclic_Buffer_Task_Type Q_Size : := 100; Q_Range Positive  1 .. Q_Size; Length : Natural 0 .. Q_Size := 0; Head, Tail : Q_Range := 1; Data : (Q_Range)  Item; Length < Q_Size => Insert (An_Item :  Item) Data(Tail) := An_Item; Insert; Tail := Tail Q_Size + 1; Length := Length + 1; Length > 0 => Remove (An_Item : Item) An_Item := Data(Head); Remove; Head := Head Q_Size + 1; Length := Length - 1; ;      ;    Cyclic_Buffer_Task_Type;

Protected types
Tasks allow for encapsulation and safe usage of variable data without the need for any explicit mutual exclusion and signaling mechanisms. Ex. 4 shows how easy it is to write server tasks that safely manage locally-declared data on behalf of multiple clients. There is no need for mutual exclusion of access to the managed data, because it is never accessed concurrently. However, the overhead of creating a task merely to serve up some data may be excessive. For such applications, Ada 95 provides protected modules, based on the well-known computer science concept of a monitor. A protected module encapsulates a data structure and exports subprograms that operate on it under automatic mutual exclusion. It also provides automatic, implicit signaling of conditions between client tasks. Again, a protected module can be either a single protected object or a protected type, allowing many protected objects to be created.

A protected module can export only procedures, functions and entries, and its body may contain only the bodies of procedures, functions and entries. The protected data is declared after private in its specification, but is accessible only within the protected module's body. Protected procedures and entries may read and/or write its encapsulated data, and automatically exclude each other. Protected functions may only read the encapsulated data, so that multiple protected function calls can be concurrently executed in the same protected object, with complete safety; but protected procedure calls and entry calls exclude protected function calls, and vice versa. Exported entries and subprograms of a protected object are executed by its calling task, as a protected object has no independent locus of control. (To be honest, optimized object code may reduce the number of context switches below the number implied by this naive description.)

Similar to a task entry which optionally has a guard, a protected entry must have a barrier to control admission. This provides automatic signaling, and ensures that when a protected entry call is accepted, its barrier condition is True, so that a barrier provides a reliable precondition for the entry body. A barrier can statically be true, then the entry is always open.

Ex. 5 The following is a simple protected type analogous to the Encapsulated_Buffer task in Ex. 2.

Protected_Buffer_Type Insert (An_Item :  Item); Remove (An_Item : Item); Buffer : Item; Empty : Boolean := True; Protected_Buffer_Type; ...    Protected_Buffer_Type Insert (An_Item :  Item) Empty Buffer := An_Item; Empty := False; Insert; Remove (An_Item : Item) Empty An_Item := Buffer; Empty := True; Remove; Protected_Buffer_Type;

Note how the barriers, using the state variable Empty, ensure that messages are alternately inserted and removed, and that no attempt can be made to take data from an empty buffer. All this is achieved without explicit signaling or mutual exclusion constructs, whether in the calling task or in the protected type itself.

The notation for calling a protected entry or procedure is exactly the same as that for calling a task entry. This makes it easy to replace one implementation of the abstract type by the other, the calling code being unaffected.

Ex. 6 The following task type implements Dijkstra's semaphore ADT, with FIFO scheduling of resumed processes. The algorithm will accept calls to both Wait and Signal, so long as the semaphore invariant would not be violated. When that circumstance approaches, calls to Wait are ignored for the time being.

Semaphore_Task_Type Initialize (N : Natural); Wait; Signal; Semaphore_Task_Type; ...    Semaphore_Task_Type Count : Natural; Initialize (N : Natural) Count := N;      Initialize; Count > 0 => Wait Count := Count - 1; Wait; Signal; Count := Count + 1; ;      ;    Semaphore_Task_Type;

This task could be used as follows:

nr_Full, nr_Free : Semaphore_Task_Type; ...  nr_Full.Initialize (0); nr_Free.Initialize (nr_Slots); ...  nr_Free.Wait; nr_Full.Signal;

Alternatively, semaphore functionality can be provided by a protected object, with major efficiency gains.

Ex. 7 The Initialize and Signal operations of this protected type are unconditional, so they are implemented as protected procedures, but the Wait operation must be guarded and is therefore implemented as an entry.

Semaphore_Protected_Type Initialize (N : Natural); Wait; Signal; Count : Natural := 0; Semaphore_Protected_Type; ...    Semaphore_Protected_Type Initialize (N : Natural) Count := N;      Initialize; Wait Count > 0 Count := Count - 1; Wait; Signal Count := Count + 1; Signal; Semaphore_Protected_Type;

Unlike the task type above, this does not ensure that Initialize is called before Wait or Signal, and Count is given a default initial value instead. Restoring this defensive feature of the task version is left as an exercise for the reader.

Entry families
Sometimes we need a group of related entries. Entry families, indexed by a discrete type, meet this need.

Ex. 8 This task provides a pool of several buffers.

Buffer_Id Integer  1 .. nr_Bufs; ...   Buffer_Pool_Task Insert (Buffer_Id) (An_Item : Item); Remove (Buffer_Id) (An_Item : Item); Buffer_Pool_Task; ...    Buffer_Pool_Task Data  :  (Buffer_Id)  Item; Filled : (Buffer_Id)  Boolean  := (others => False); I Data' Filled(I) => Insert (I) (An_Item : Item) Data(I) := An_Item; Insert; Filled(I) := True; Filled(I) => Remove (I) (An_Item : Item) An_Item := Data(I); Remove; Filled(I) := False; ;          ;        ;      ;       Buffer_Pool_Task; ...  Buffer_Pool_Task.Remove(K)(This_Item);

Note that the busy wait  is necessary here to prevent the task from being suspended on some buffer when there was no call pending for it, because such suspension would delay serving requests for all the other buffers (perhaps indefinitely).

Termination
Server tasks often contain infinite loops to allow them to service an arbitrary number of calls in succession. But control cannot leave a task's master until the task terminates, so we need a way for a server to know when it should terminate. This is done by a terminate alternative in a selective wait.

Ex. 9

Terminating_Buffer_Task_Type Insert (An_Item :  Item); Remove (An_Item : Item); Terminating_Buffer_Task_Type; ...    Terminating_Buffer_Task_Type Datum : Item; Insert (An_Item :  Item) Datum := An_Item; Insert; ;         ;             Remove (An_Item :  Item) An_Item := Datum; Remove; ;         ;       ;    Terminating_Buffer_Task_Type;

The task terminates when:
 * 1) at least one terminate alternative is open, and
 * 2) there are no pending calls to its entries, and
 * 3) all other tasks of the same master are in the same state (or already terminated), and
 * 4) the task's master has completed (i.e. has run out of statements to execute).

Conditions (1) and (2) ensure that the task is in a fit state to stop. Conditions (3) and (4) ensure that stopping cannot have an adverse effect on the rest of the program, because no further calls that might change its state are possible.

Timeout
A task may need to avoid being held up by calling to a slow server. A timed entry call lets a client specify a maximum delay before achieving rendezvous, failing which the attempted entry call is withdrawn and an alternative sequence of statements is executed.

Ex. 10

Password_Server Check (User, Pass : String; Valid :  Boolean); Set (User, Pass :  String); Password_Server; ...  User_Name, Password : String (1 .. 8); ...  Put ("Please give your new password:"); Get_Line (Password); Password_Server.Set (User_Name, Password); Put_Line ("Done"); 10.0;     Put_Line ("The system is busy now, please try again later."); ;

To time out the functionality provided by a task, two distinct entries are needed: one to pass in arguments, and one to collect the result. Timing out on rendezvous with the latter achieves the desired effect.

Ex. 11

Process_Data Input (D :   Datum); Output (D :  Datum); Process_Data; Input_Data, Output_Data : Datum; collect Input_Data from sensors; Process_Data.Input (Input_Data); Process_Data.Output (Output_Data); pass Output_Data to display task; 0.1;        Log_Error ("Processing did not complete quickly enough."); ;   ;

Symmetrically, a delay alternative in a selective wait statement allows a server task to withdraw an offer to accept calls after a maximum delay in achieving rendezvous with any client.

Ex. 12

Resource_Lender Get_Loan (Period : Duration); Give_Back; Resource_Lender; ...    Resource_Lender Period_Of_Loan : Duration; Get_Loan (Period : Duration) Period_Of_Loan := Period; Get_Loan; Give_Back; Period_Of_Loan; Log_Error ("Borrower did not give up loan soon enough."); ;           ;          ;       ;    Resource_Lender;

Conditional entry calls
An entry call can be made conditional, so that it is withdrawn if the rendezvous is not immediately achieved. This uses the select statement notation with an else part. Thus the constructs

Callee.Rendezvous; Do_something_else; ;

and

Callee.Rendezvous; 0.0;    Do_something_else; ;

seem to be conceptually equivalent. However, the attempt to start the rendezvous may take some time, especially if the callee is on another processor, so the delay 0.0; may expire although the callee would be able to accept the rendezvous, whereas the else construct is safe.

Requeue statements
A requeue statement allows an accept statement or entry body to be completed while redirecting it to a different or the same entry queue, even to one of another task. The called entry has to share the same parameter list or be parameter-less. The caller of the original entry is not aware of the requeue and the entry call continues although now to possibly another entry of another task.

The requeue statement should normally be used to quickly check some precondition for the work proper. If these are fulfilled, the work proper is delegated to another task, hence the caller should nearly immediately be requeued.

Thus requeuing may have an effect on timed entry calls. To be a bit more specific, say the timed entry call is to T1.E1, the requeue within T1.E1 to T2.E2:

T1   ... E1     ...        T2E2 E1  ... T1

Let Delta_T be the timeout of the timed entry call to T1.E1. There are now several possibilities:

1. Delta_T expires before T1.E1 is accepted.
 * The entry call is aborted, i.e. taken out of the queue.

2. Delta_T expires after T1.E1 is accepted.
 * T1.E1 has finished (its check of preconditions) and T2.E2 is to be accepted.
 * For the caller, who is unaware of the requeue, the entry call is still executing; it is completed only with the completion T2.E2.

Thus, although the original entry call may be postponed for a long time while T2.E2 is waiting to be accepted, the call is executing from the caller's point of view.

To avoid this behaviour, a call may be requeued with abort. This changes case 2 above:

2.a The call is requeued to T2.E2 before Delta_T expires.


 * 2.a.1. T2.E2 is accepted before expiration, the call continues until T2.E2 is completed.


 * 2.a.2. Delta_T expires before T2.E2 is accepted: The entry call is aborted, i.e. taken out of the queue of T2.E2.

2.b The call is requeued to T2.E2 after the expiration of Delta_T.


 * 2.b.1. T2.E2 is immediately available (i.e. there is no requeue), T2.E2 continues to completion.


 * 2.b.2. T2.E2 is queued: The entry call is aborted, i.e. taken out of the queue of T2.E2.

In short, for a requeue with abort, the entry call to T1.E1 is completed in cases 1, 2.a.1 and 2.b.1; it is aborted in 2.a.2 and 2.b.2.

So what is the difference between these three entries?

E1     ...        T2E2 E1   E2      ...       T2E2 E2   E3      ...      E3   T2E2

E1 has just been discussed. After the requeue, its enclosing task is free for other work, while the caller is still suspended until its call is completed or aborted.

E2 also delegates, however via an entry call. Thus E2 completes only with the completion of T2.E2.

E3 first frees the caller, then delegates to T2.E2, i.e. the entry call is completed with E3.

Scheduling
FIFO, priority, priority inversion avoidance, ... to be completed.

Interfaces and polymorphism
Tasks and protected types can also implement interfaces.

Printable  ; Input (D :   Printable); Process_Data  Printable Input (D  :   Datum); Output (D :  Datum); Process_Data;

To allow delegation necessary to the polymorphism, the interface Printable shall be defined in its own package. It is then possible to define different task type implementing the Printable interface and use these implemetations polymorphically:

printable_package; printable_package; Printer Print_Red   Printable  ; Print_Blue  Printable  ; Print_Red Ada.Text_IO.Put_Line ("Printing in Red"); Print_Red; Print_Blue Ada.Text_IO.Put_Line ("Printing in Blue"); Print_Blue; printer_task : Printable'Class; printer_task := Print_Red; printer_task := Print_Blue; Printer;

This feature is also called synchronized interfaces.

Restrictions and Profiles
Ada tasking has too many features for some applications. Therefore there exist restrictions and profiles for certain, mostly safety or security critical applications. Restrictions and profiles are defined via pragmas. A restriction forbids the use of certain features, for instance the restriction No_Abort_Statements forbids the use of the abort statement. A profile (do not confuse with parameter profiles for subprograms) combines a set of restrictions.

See

Wikibook

 * Ada Programming
 * Ada Programming/Libraries/Ada.Storage IO
 * Ada Programming/Libraries/Ada.Task_Identification
 * Ada Programming/Libraries/Ada.Task_Attributes

Ada Quality and Style Guide

 * Chapter 4: Program Structure
 * Chapter 6: Concurrency
 * Chapter 6: Concurrency
 * Chapter 6: Concurrency

|Tasking

Programación en Ada/Tareas