Although the GNAT run-time reuses most of the support provided by the low level Pthreads library, it needs to handle some additional information to provide the full Ada semantics: state of the Ada task (according to the Ada semantics), parent, activator, etc. This information is stored by the run-time in a per-task register called Ada Task Control Block (ATCB)2.1. In addition, some task specific information is also required to store the task discriminants (the task parameters). The compiler generates code which creates another register for such information. The ATCB is linked with this register and with the corresponding Threads Control Block (TCB) in the POSIX level (cf. Figure ).
When a task is created, the run-time dynamically generates a new ATCB and inserts it in a list (All Tasks List2.2). ATCBs are always inserted in LIFO order (as a stack). Therefore, the first ATCB in this list corresponds to the most recently created task.
GNAT considers four basic states during task life (indicated by the State ATCB field):
The sleep state is composed of the following sub-states:
The master is a construct used to finalize local objects, including tasks (see section ). Each master handles the termination of an Ada scope to ensure the Ada tasks termination semantics (all dependent tasks must terminate before its master performs finalization on other objects that it created). It is associated with the scope being executed by the Parent when the task was created. The run-time is only concerned with masters for purposes of task termination.
GNAT associates one identifier to each master, and two values are associated with each task: the master of its Parent (Master_Of_Task) and its internal master nesting level (Master_Within).
New_Task.Master_Of_Task = Activator.Master_Within
Tasks created by an allocator do not necessarily depend on its activator; in such case the activator's termination may precede the termination of the newly created task [AAR95, section 9.2(5a)] Therefore, the master of a task created by the evaluation of an allocator is the declarative region which contains the access type definition. Tasks declared in library-level packages have the main program as their master. That is, the main program can not terminate until all library-level tasks have terminated [BW98, chapter 4.3.2]. Given a task T, table presents a summary of the basic concepts used by the run-time for handling Ada task termination.
In order to understand these concepts better, let's apply them to the following example.
procedure P is -- P: Parent = Environment Task; -- Activator = Environment -- Master_Of_Task = 1; Master_Within = 2; task T1; -- T1: Parent = P; Activator = P -- Master_Of_Task = 2; Master_Within = 3; task body T1 is task type TT; task body TT is begin null; end TT; type TTA is access TT; T2 : TT; -- T2: Parent = T1; Activator = T1 -- Master_Of_Task = 3; Master_Within = 4; task T3; -- T3: Parent = T1; Activator = T1 -- Master_Of_Task = 3; Master_Within = 4; task body T3 is task T4; -- T4: Parent = T3; Activator = T3 -- Master_Of_Task = 4; Master_Within = 5; task body T4 is begin null; end T4; T5 : TT; -- T5: Parent = T3; Activator = T3 -- Master_Of_Task = 4; Master_Within = 5; T6 : TTA := new TT; -- T6: Parent = T1; Activator = T3 -- Master_Of_Task = 2; Master_Within = 3; begin null; end T3; begin null; end T1; begin null; end P;
Parent and activator do not coincide in T6 because the task is created by means of an Ada allocator (an Ada expression in the form 'new ...'). In this case the parent of the new task is the task where the type is declared and the activator is the task which executes the allocator. In the other cases, parent and activator coincide.
In order to understand the run-time behavior we first present the task translation done by the compiler.
The Ada task type is translated by the compiler into a limited record
with the same discriminants. For example, the following task specification:
task type T_Task (Discriminant : DType) is ... end T_Task;...is translated by the compiler into the following code:
T_TaskE : aliased Boolean := False; T_TaskZ : Size_Type := [ Unspecified_Size | Size_Type (Size_Expression) ]; type T_TaskV [ (Discriminant : DType) ] is limited record _Task_Id : System.Tasking.Task_Id; [ Entry_Family : array (Bounds) of Void; ] [ _Priority : Integer := Priority_Expression; ] [ _Size : Size_Type := Size_Expression; ] [ _Task_Info : Task_Info_Type := Task_Info_Expression; ] [ _Task_Name : Task_Image_Type : new String'(Task_Name); ] end record;
The optional code (the code that it is not always generated by the
compiler) has been put between square brackets (
a boolean flag
E is declared and initialized to false. It is
set to True when the body of the task is elaborated. The
Z variable holds the task stack size (either the default value,
unspecified_size, or the value set by a pragma Storage_Size). Next the task type is translated by the compiler into
a limited record
V with Discriminants present only if the
corresponding task type has discriminants. The first field contains
value (an access to the corresponding ATCB). One Entry_Family
component is present for each entry family in the task definition. The
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 the element type is void. The next three
fields are present only if the corresponding pragma is present in the
task definition: the Size field corresponds to Storage_Size pragma; Task_Info corresponds to Task_Info pragma, and Task_Name corresponds to Task_Name pragma. A reference to this record is stored in the
(cf. Figure ). This reference is used by the thread
associated with the task to find the task discriminants.
The run-time needs an access to subprogram to call the task user
code. Therefore the compiler translates the task body into a
procedure. For example, the following task body:
task body T_Task is <Declarations> begin <Statements> end T_Task;...is translated by the compiler into the following code:
1: procedure T_TaskB (_Task : access T_TaskV) is 2: Discriminant : Dtype renames _Task.Discriminant; 3: 4: procedure _Clean is 5: begin 6: Abort_Defer; 7: GNARL.Complete_Task; 8: Abort_Undefer; 9: end _Clean; 10: 11: begin 12: Abort_Undefer; 13: <Declarations> 14: [ Activate_Tasks ] 15: GNARL.Complete_Activation; 16: <Statements> 17: at end 18: _Clean; 19: end T_TaskB;
The call to Activate_Tasks2.5 (line 14) is only generated if the task body is an activator. The at end handler is a single point of task finalization that is called even in the presence of exceptions or task abortion [BG94, section 6.9.5].
Figure presents the sequence of calls to the run-time issued by the compiler generated code during the creation and finalization of a task. Each rectangle represents a subprogram.
The whole sequence is as follows:
The thread associated with the new task executes a Task_Wrapper procedure. This procedure has some locally declared objects that serve as per-task run-time local data. The Task Wrapper calls the Task Body Procedure (the procedure generated by the compiler which has the task user code) which elaborates the declarations within the task declarative part, setting up the local environment in which it will later execute its sequence of statements. (In general the compiler must generate code for the elaboration of Ada declarations.) Note that if these declarations also have task objects, then there is a chained activation: this task becomes the activator of dependent task objects and can not start the execution of its user code until all dependent tasks complete their activation.
From here the activator and the new tasks proceed concurrently and their execution is controlled by the POSIX scheduler. Afterward, any of them can terminate their execution and therefore the following two steps can be interchanged.
In general this is the earliest point at which it is completely safe to discard all storage associated with its dependent tasks (because it is at this point that execution leaves the scope of the task's type declaration). This is so because reference to a task may be passed far from its point of creation, as via task access variables and functions returning task values [BR85, Section 4].
In the following sections we make a detailed description of the work done by the following run-time subprograms: Enter Master, Create Task, Activate Tasks, Complete Activation, Complete Task, and Complete Master.
Enter_Master2.6 just increments the current value of Master_Within in the activator.
Create_Task2.7 carries out the following actions:
The priority of a task where no priority is specified is the priority at which it was created, that is, the activator priority at the time that it calls Create_Task [BG94, section 6.7.5]
From this point the new task becomes callable. When the call to this run-time subprogram returns the code generated by the compiler executes a sentence which sets to True the variable which reminds that the task has been elaborated (described in section ).
With respect to the tasks activation the Ada Reference Manual says that ``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)]
GNAT uses an auxiliary list (the Activation List) to achieve this semantics. In a first stage all the ATCBs are created and inserted in the two lists (All Tasks and Activation lists); in a second stage the Activation List is traversed and new threads of control are created and associated with the new ATCBs. Although ATCBs are inserted in both lists in LIFO order (as a stack) all activated tasks synchronize on the activators lock before they start their activation in priority order. The activation chain is not outstanding when all its tasks have been activated.
Activate_Tasks2.11 performs the following actions:
For the activation of a task, the activator checks that the task_body is already elaborated. If two or more tasks are being activated together (see ARM 9.2), as the result of the elaboration of a declarative_part or the initialization for the object created by an allocator, this check is done for all of them before activating any.
Reason: As specified by AI-00149, the check is done by the activator, rather than by the task itself. If it were done by the task itself, it would be turned into a Tasking_Error in the activator, and the other tasks would still be activated [AAR95, section 3.11(12)].
GNULLcall2.12 and associates it to the task wrapper. If the creation of the new thread fails, release the locks and set the caller ATCB field Activation_Failed to True.
Once all of these activations are complete, if the activation of any of the tasks has failed (due to the propagation of an exception), Tasking_Error is raised in the activator, at the place at which it initiated the activations. Otherwise, the activator proceeds with its execution normally. Any task aborted prior to completing their activation are ignored when determining whether to raise Tasking_Error [AAR95, section 9.2(5)].
Complete_Activation2.13 is called by each task when it completes the elaboration of its declarative part. It carries out the following actions:
The Complete_Task2.14 subprogram performs the following single action:
From this point the task becomes not callable.
The run-time subprogram Complete_Master2.15 carries out the following actions: