Ada tasks semantics requires run-time support for storage allocation, task scheduling, and intertask communication. These functions are typically performed by the kernel of the operating system. Ada is so specific in its semantic requirements, however, that it is unlikely that a given existing operating system will make such services available in a form that can be directly used by the generated code. As a consequence, the compiler run-time must add routines that support Ada tasking semantics on top of OS primitives, or else provide a tasking kernel for applications that run on bare boards.
The GNAT run-time assumes that functionality equivalent to that of the POSIX threads library (pthreads) is available in the target system. The additional 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).
The Ada Task Control Block (ATCB) is a discriminated record, whose discriminant is the number of task entries (a central component of the ATCB is the array of queues whose size is fixed by the discriminant). In order to support Ada95 task discriminants and some Ada task attributes, the front-end generates an additional record (described in detail in Section 9.4). When a task is created, the run-time generates a new ATCB and links the new ATCB with its corresponding high-level record and Threads Control Block (TCB) record in the POSIX level (cf. Figure 14.1). In addition, the GNAT run-time inserts the new ATCB in a linked list (All Tasks List). 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 over the lifetime of a task. The current state is indicated by the State ATCB field):
The sleep state is composed of the following sub-states:
According to Ada semantics, all tasks created by the elaboration of object declarations of a single declarative region (including subcomponents of 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 in the current scope, that holds the activation list (cf. Sections 9.1 and 9.6), 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.
With respect to task termination, the concept of a master [AAR95, Section 9.3] is fundamental to the semantics of the language. (cf. Section 9.2). Basically a master defines a scope at the end of which the run-time must wait for termination of dependent tasks, before finalization of other objects created on such a scope. To implement this behavior, the front-end generates calls to the run-time subprograms Enter_Master and Complete_Master at the beginning and termination of a master scope (or, in the case of tasks, via Create_Task and Complete_Task subprograms). ^oThe GNAT run-time associates one identifier to each master, and two master identifiers with each task: the master of its Parent (Master_Of_Task) and its internal master nesting level (Master_Within). The identification method of masters provides an ordering on them, so that a master that depends on another will always have associated an identifier higher than that of its own master.
Normally a task starts out with internal master nesting level one larger than external master nesting level. This value is incremented by Enter_Master, which is called if the task itself has dependent tasks. It is set to 1 for the environment task. The environment task is 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. Master level 2 is reserved for server tasks of the run-time system (the so called "independent tasks"), and the level 3 is for the library level tasks.
Tasks created by an allocator do not necessarily depend on their activator, but rather on the master that created the access type; 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 cannot terminate until all library-level tasks have terminated [BW98, Chapter 4.3.2]. Figure 14.2 summarizes 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:
Parent and activator do not coincide for T6 because the task is created by means of an allocator, and in this case the parent of the new task is the task where the access type is declared, while the activator is the task that executes the allocator. In all other cases above, parent and activator coincide.
The abort statement is intended for use in response to those error conditions where recovery by the errant task is deemed to be impossible. The language defines some operations in which abortion must be deferred [BW98, Section 10.2.1]. In addition, the execution of some critical points of the run-time must be also deferred to keep its state stable. For this purpose the GNAT run-time uses a pair of subprograms (Abort_Defer, Abort_Undefer) that are called by the code expanded by the front-end to bracket unabortable operations involving task termination, (cf. Section 9.5), rendezvous statements (cf. Chapter 10), and protected objects (cf. Chapter 11). The implementation of these primitives will be discussed in detail in Chapter 20.
Section 9.6 discussed the sequence of calls to the run-time issued by the expanded code at the point of tasks creation and finalization. Figure 14.3 represents this sequence. Each rectangle represents a subprogram; the rhombus represents the new task.
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, to set up the local environment in which it will later execute its sequence of statements. 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 cannot start the execution of its user code until all dependent tasks complete their own activation.
From here on the activator and the new tasks proceed concurrently and their execution is controlled by the POSIX scheduler. Afterward, any of them can terminate its 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 dependent tasks (because it is at this point that execution leaves the scope of the task's declaration, and it is no longer possible for any dependent task to be awakened again by a call).
In the following sections we give more 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, which implement the most important aspects of tasking.
Enter_Master increments the current value of Master_Within in the activator.
Create_Task carries out the following actions:
From this point the new task becomes callable. When the call to this run-time subprogram returns, the code generated by the compiler sets to True the variable which indicates that the task has been elaborated.
With respect to task 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 all activated tasks synchronize on the activators lock before they start their activation in priority order. The activation chain is not preserved once all its tasks have been activated.
Activate_Tasks 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 of 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)].
GNARLcall (cf. Create_Task) 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 (typically 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 tasks aborted prior to completing their activation are ignored when determining whether to raise Tasking_Error [AAR95, Section 9.2(5)].
The task-wrapper is a GNARL procedure that has some local objects that serve as per-task local data.
Complete_Activation is called by each task when it completes the elaboration of its declarative part. It carries out the following actions:
The Complete_Task subprogram performs the following single action:
From this point the task becomes not callable.
The run-time subprogram Complete_Master carries out the following actions:
In this chapter we have examined the basic data structures used by the GNAT run-time to support Ada tasks, the task states used by GNARL, some of the compiler-generated code that invokes run-time actions, and the subprograms called by this generated code. To summarize again: