A task type is a template from which actual task objects are created. A task object can be created either as part of the elaboration of an object declaration occurring immediately within some declarative region, or as part of the evaluation of an allocator (an expression in the form ``new...''). The Parent is the task on which a task depends. If the task has been declared by means of an object declaration, its parent is the task which declared the task object; if the task has been declared by means of an allocator (an Ada expression in the form 'new ...'), its parent is the task which has the corresponding access declaration. When a parent creates a new task, the parent's execution is suspended while it waits for the child to complete its activation (either immediately, if the child is created by an allocator, or after the elaboration of the associated declarative part). Once sevthe child has finished its activation, parent and child proceed concurrently. If a task creates another task during its own activation, then it must also wait for its child to activate before it can begin execution (cf. [BW98, Chapter 4.3.1] and [Bar99, Chapter 17.7]).
Conceptually every Ada program has a task (called the Environment Task) which is responsible for the program elaboration. The environment task is generally the operating system thread which initializes the run-time and executes the main Ada subprogram. Before calling the main procedure of the Ada program, the environment task elaborates all library units needed by the Ada main program. This elaboration causes library-level tasks to be created and activated before the main procedure starts execution.
The lifetime of a task object has three main phases: (1) Activation, elaboration of the declarative part of the task body. The Activator denotes the task which created and activated the task. All tasks created by the elaboration of object declarations of a single declarative region (including subcomponents of the declared objects) are activated together. Similarly, all tasks created by the evaluation of a single allocator are activated together [AAR95, Section 9]; (2) Normal execution, execution of the statements visible within the body of the task; and (3) Finalization, execution of any finalization code associated with the objects in its declarative part.
In order to provide this run-time semantics, the front-end performs substantial transformations on the AST, and adds numerous calls to run-time routines. Each tasking primitive (rendez-vous, selective wait, delay statement, abort, etc.) is expanded into generally target-independent code. The run-time information concerning each Ada task (task state, parent, activator, etc. [AAR95, Chapter 9]) is stored in a per-task record called the Ada Task Control Block (ATCB) (cf. Section 14.1. In the examples that follow, the interface to the run-time is described generically by the name GNARL, which in practice corresponds to a collection of run-time packages with the same interface, and bodies that are OS-dependent.
Details are provided in the following sections. The description unavoidably combines issues of run-time semantics with analysis and code expansion.
According to Ada semantics, all tasks created by the elaboration of object declarations of a single declarative region (including subcomponents of the declared objects) are activated together. Similarly, all tasks created by the evaluation of a single allocator are activated together [AAR95, Section 9.2(2)]. In addition, if an exception is raised in the elaboration of a declarative part, then any task T created during that elaboration becomes terminated and is never activated. As T itself cannot handle the exception, the language requires the parent (creator) task to deal with the situation: the predefined exception Tasking_Error is raised in the activating context.
In order to achieve this semantics, the GNAT run-time uses an auxiliary list (the Activation List). The front-end expands the object declaration by introducing a local variable that holds the the activation list, and splits the OS call to create the new thread into two separate calls to the GNAT run-time: (1) Create_Task, which creates and initializes the new ACTB, and inserts it into both the all-tasks and activation lists, and (2) Activate_Task, which traverses the activation list and activates the new threads.
The concept of a master is the basis of the rules for task termination in Ada (cf. [AAR95, Section 9.3] ). A Master is a construct that performs finalization of local objects after it is complete, but before leaving its execution altogether. For finalization purposes, a master can be a task body, block statement, subprogram body i.e. any construct that contains local declarations. The language also considers accept statements as masters because the body of an accept may contain aggregates and function calls that create anonymous local objects that may require finalization. Task objects declared by the master, or denoted by objects of access types declared by the master, are said to depend on that master. Dependent tasks must terminate before a master performs finalization on other objects that it has created. Since accept statements have no declarative part, tasks are never dependent on them, and so a master for purposes of task termination is a task body, block statement, or subprogram body.
To properly implement task termination, the run-time must be notified of the change in master-nesting whenever execution is about to enter or leave a nontrivial master. Therefore, in this case the front-end generates calls to the run-time subprograms Enter_Master and Complete_Master (or, in the case of tasks, via Create_Task and Complete_Task subprograms).
The abort statement is intended for use in response to those error conditions where recovery by the errant task is deemed to be impossible. Tasks which are aborted are said to become abnormal, and are thus prevented from interacting with any other task. Ideally, an abnormal task will stop executing immediately. However, certain actions must be protected in order that the integrity of the run-time be assured. For example, the following operations are defined to be abort-deferred [BW98, Section 10.2.1]: (1) a protected action, (2) waiting for an entry call to complete, (3) waiting for termination of dependent tasks, and (4) the execution of the controlled-types initialize, finalize, and adjust subprograms. This means that execution of abnormal tasks that are performing one of these operations must continue until the operation is complete.
For this purpose the GNAT run-time has a pair of subprograms (Abort_Defer, Abort_Undefer) which are used by the code expanded by the front-end for the task body (cf. Section 9.5), the rendezvous statements (cf. Chapter 10), and protected objects (cf. Chapter 11). Intuitively, Abort_Defer masks all interrupts until Abort_Undefer is invoked.
A task type declaration is expanded by the front-end into a limited record type declaration. For example, let us consider the following task specification:
It is expanded by the front-end into the following code:
The code between brackets is optional; its expansion depends on the
Ada features appearing in the source code. The rest is needed at
run-time for all tasks: let us examine this expansion in detail. First
we find two variable declarations: (1) a boolean flag
will indicate if the body of the task has been elaborated, and (2) a
variable which holds the task stack size (either the default value,
unspecified_size, or the value set by means of a pragma Storage_Size).
Each task type is expanded by the front-end into a separate limited
V. If the task type has discriminants, this record type
must include the same discriminants. The first field of the record
contains the Task_ID9.1
value (an access to the corresponding ATCB,
cf. Section 14.1). If the task type
has entry families, one Entry_Family_Name component is present
for each entry family in the task type definition; their bounds
correspond to the bounds of the entry family (which may depend on
discriminants). Since the run-time only needs such information for
determining the entry index, their element type is void. Finally, the
next three fields are present only if the corresponding Ada pragma is
present in the task definition: pragmas Storage_Size, Task_Info, and Task_Name.
The task body is expanded into a procedure. For example, let us consider the following task body:
It is expanded by the front-end into the following code:
The procedure receives a single parameter _Task, which is an access to the corresponding high-level record (also generated by the expander, cf. section 9.4). This parameter gives access to the discriminants of the task object, which can be used in the code of the body. The renaming (line 2) simplifies the access to the discriminants in the expanded code. The local subprogram _Clean is a common point of finalization; by means of the special at end statement, it is called at the end of the subprogram execution whether the operation completes successfully or abnormally.
Once the task is activated, it first executes code that elaborates its local objects (line 13), and then calls a run-time subprogram (Complete_Activation) to notify the activator that the elaboration was successfully completed. At this point the task becomes blocked until all tasks created by the activator complete their elaborations; if any of them fails, the task is aborted and Tasking_Error is raised in the activator. If the task is not aborted, it is allowed to proceed and execute its statements (line 15) under control of the run-time scheduler. When the task terminates (successfully or abnormally), the local subprogram _Clean is executed, which calls the run-time to perform its finalization. Even though a completed task cannot execute any longer, it is not yet safe to deallocate its working storage at this point because some reference to the task may exist in other tasks. In particular, it is possible for other tasks to still attempt entry calls to a terminated task, to abort it, and to interrogate its status via the 'Terminated and 'Callable attributes. For this reason, the resources are not deallocated until the master associated with the task completes. In general this is the earliest point at which it is completely safe to discard all storage associated with dependent tasks, because it is at this point that execution leaves the scope of the task's type declaration, and there is no longer any way to refer to the task. Any references to the task that may have been passed far from its point of creation, as via access variables or functions returning task values [BR85, Section 4] are themselves dead at this point.
To help the reader to understand the sequence of run-time actions involved in the life-time of Ada tasks, let us summarize the code expansion presented in Sections 9.1, 9.4, and 9.5 by means of a simple example. Let us consider the following Ada code:
This code is expanded by the GNAT front-end as follows:
The numbers in the comments to the right of the code present the execution sequence. First, because the main procedure has a task object declaration, it notifies the run-time that it is executing a master scope (step 1). It then creates the task ATCB (step 2), and activates the corresponding thread (step 3). After the task completes the elaboration of its local objects, it calls the run-time to report that it has completed its activation (step 4). From here on the execution of the task body and the main subprogram proceed in parallel (steps 5'). When the task terminates it notifies the run-time of its termination (step 6). When the body of the activator completes, it calls the run-time to wait for dependent tasks that may not have completed (step 7). Once it is established that the dependent task has terminated, the run-time recovers the task resources, and leaves the activator to terminate. If the activator calls Complete_Master before the dependent task completes its execution, the activator is blocked by the run-time until the dependent task body notifies its termination.
In this chapter we have seen the basic data structures used to support Ada tasks, and the corresponding task expansion. The run-time associates to each task an Ada Task Control Block (ATCB). Although the run-time registers the ATCBs in a linked list, one auxiliary list is required to implement the Ada semantics for tasks activation. Therefore, the front-end generates a temporal variable used to reference the elements in this list, and the run-time calls to create and activate the tasks. The front-end also generates calls to the run-time at the points at which the user code enters or leaves a master scope.
The Ada task specification is expanded into a limited record. The Ada task body is expanded into a procedure with calls to the run-time to notify: the successfully task activation, and the task termination.