At some predefined points the abortion can not be immediately attended. For example, abort deferral is required by the language for protected actions and finalization routines. It is generally also required during the execution of the Ada run-time, to ensure the integrity of run-time data structures. Implementing abort deferral can be divided into two parts [GB94b, Section 3.3]:
In general, the determination of whether a given task is abort-deferred must be carried out by the task itself. In a single-processor system, it may be possible for the task initiating an abort to determine whether the target task is abort-deferred. However, in a multi-processor system, or a single processor system where the Ada run-time is not in direct control of task scheduling, this is not possible. The abort-deferral state of the target task may change between the point it is tested and the point the target task is interrupted [GB94b, Section 3.3].
There are two obvious techniques for recording whether a task is abort-deferred. One technique is sometimes termed PCmapping. The compiler and link-editor generate a map of abort-deferred regions. Whether the task is abort-deferred can then be determined by comparing the current instruction-pointer value, and all the saved return addresses of active subprogram calls, against the map. To ensure the abort is processed on exit from the abort-deferred region, one overwrites the saved return address of the outermost abort-deferred call frame with the address of the abort-processing routine (saving the old return address elsewhere). The test for abort deferral may take time proportional to the depth of subprogram call nesting, but that occurs only if an ATC is attempted. Until that occurs, no runtime overhead is incurred for abort deferral. A restriction of this method is that abort-deferred regions must correspond to callable units of code. Another restriction is that the subprogram calling convention is constrained to (1) ensure the return addresses are always in a predictable and accessible location and (2) ensure this data is always valid, even if the calling sequence is interrupted. Unfortunately, that is not true for some architectures [GB94b, Section 3.3].
In the other technique the task increments and decrements a deferral nesting level (e.g. in a dedicated register or the ATCB), whenever it enters and exits an abort-deferred region. On exit from such a region, if the counter goes to zero, the task must check whether there is a pending abort and, if so, process the abort. This deferral-counter method imposes some distributed overhead on entry and exit of abort-deferred regions, but allows GNARL quick checking [GB94b, Section 3.3]. This is the technique used by the GNAT run-time. GNAT Undefer_Abort8.1 subprogram is the universal polling point for deferred processing. It is responsible for:
If some request is made to modify the priority of a task, or to abort an abort-deferred task, the ATCB field Pending_Action is set to True (and the abortion will be done later by the GNARL Undefer_Abortion procedure).
In general, processing an abort requires unwinding the stack of the target task, rather than immediately jumping out of the aborted part (or killing the task, in the case of entire-task abortion). There may be local controlled objects, which require the execution of a finalization routine. There also may be dependent tasks, which require the aborted processing block until they have been aborted, finalized, and terminated. The finalization must be done in LIFO order and the stack contexts of the objects requiring finalization must be preserved until the objects are finalized [GB94b, Section 3.4]
The GNARL implementation of the Ada abort statement is made up of:
Figure presents the sequence of run-time subprograms involved in the task abortion. The GNARL procedure Locked_Abort_To_Level8.2 sets to true the ATCB flag Pending_Action. and, depending on the current state of the target task (blocked or running) it calls Wakeup8.3 or Abort_Task8.4.
In both cases the internal exception Abort_Signal can not be handled by the user defined exception handlers and unwinds the stack of the aborted task.
An implementation of ATC must address the following issues [GB94b, section 3]:
Since ATC is not likely to be used in most (non real-time) Ada programs, a key objective of any implementation should be to impose little or no distributed overhead for the existence of this feature. Subject to this constraint, the implementation of ATC should be as efficient as possible [GB94b, section 3].
There are two implementation models for the ATC, which can be classified according to the number of threads required for its implementation. One thread model and two threads model.
In this model the caller starts with the triggering statement (basically it executes the steps described in ). If it can be completed immediately, the abortable part is never executed at all; the task continues with the triggered statements and then jumps to the end of the select statement. Otherwise, if the entry call remains abortably queued the task does not suspend execution; instead it proceeds to execute the abortable part. If triggering event occurs before the task completes the abortable part, the task is interrupted and forced to first execute finalization code for the abortable part, then transfer control to the triggered statements. Otherwise, if the abortable part completes before the completion of the entry call, an attempt is made to cancel the entry call and, if successful, jumps to the end of the select statement [BW98, section 10.3.2] [GB94b, section 3.1].
Similarly, if the triggering statement is a delay statement, the delay time is calculated and, if it has not passed, the abortable part is then executed. If this finishes before the delay time expires, the delay is canceled and the execution of the asynchronous select statement is finished [BW98, section 10.3.2].
If the cancellation of the triggering event fails, because the protected action or rendezvous has started, or has been requeued (without abort), then the asynchronous select statement waits for the triggering statement to complete before executing the optional sequence of statements following the triggering statement [BW98, section 10.3.2].
In this model, before blocking on the triggering statement, the task executing the asynchronous select creates an agent thread of control to execute the abortable part. The ATC is carried out by aborting the agent thread, if the original thread wakes up at the triggering statement before the agent completes [GB94b, section 3.1].
Proponents of the two-thread model have argued that it simplifies the implementation of several aspects. One is that it preservers two useful invariants of the original Ada tasking model namely: (1) a thread that is waiting for an event is not executing; (2) a thread never waits for more than a timeout and one other event. Another simplification is that the two thread model eliminates the need for one thread to asynchronously modify another thread's flow of control, which is not possible in some execution environments. If there is a way to kill a thread, it should be sufficient to simply kill the agent thread and wake up the client [GB94b, section 3.1].
The two-thread model seems to complicate the implementation at least as much as it simplifies it. It violates a key invariant of Ada tasking, that there is a one-to-one correspondence between tasks and threads of control. This assumption pervades the semantics, and is the foundation of existing Ada tasking implementations. Loss of this invariant has many ramifications. Among these, data that previously could only be accessed by one thread of control becomes susceptible to race conditions. Thus, there are new requirements for synchronization, and new potential for deadlock within a single task. Also, just killing the agent thread is not such a simple solution as it might seem. There remains the problem of how to execute the agent's finalization code. If the operation that kills a thread does not support finalization, some other thread must perform the finalization. To do so, it must wait for the killed thread to be terminated to be able to obtain access to the run-time stack of the terminated thread. The latter may not be possible in systems where killing a thread also releases its stack space [GB94b, section 3.1].
The one-thread model can be implemented using a signal and longjmp(). The trigger (entry call or delay) is pending on the thread while the abortable part is executed. If the abortable part completes first, the pending trigger is removed. If the trigger completes, an abortion signal is sent to the thread. The signal handler for the abortion signal then transfers control out of the abortable part into the triggered statements [GMB93, section 4.3]. Due to the disadvantages of the two-threaded model, GNAT implements the one-thread model. The non-local jump is performed by raising the internal exception in a signal handler. The propagation of this exception aborts one or more levels of abortable parts [GB94a, section 4.3].
ATCs can be nested. This allows a task to issue another entry call while it is waiting to complete a previous entry call (in the abortable part of the ATC). Therefore, the Ada run-time must store all these pending entry calls. The GNAT run-time associates an Entry Call Stack to each Ada task (Entry_Calls ATCB field--figure ). The top of this stack (ATC_Nesting_Level ATCB field) is initialized to 1, indicating that the task has not issued any entry call. Before an entry call, the task increments ATC_Nesting_Level. Therefore, level 1 is not used. The Pending_ATC_Level field is used to signal an abort. In order to distinguish between the Abort statement and the end of an asynchronous request the GNAT run-time defines the following rule:
Below we present the translation done by the compiler to implement the
declare P : Parms := (Parm1, Parm2, ..., ParmN); Successful : Boolean; begin GNARL.Defer_Abortion; GNARL.Task_Entry_Call (Task_ID, Entry_ID, P'Address, Successful); begin -- Abortable Part Scope begin GNARL.Undefer_Abortion; << Abortable Part >> at end GNARL.Entry_Call_Cancellation end; exception when Abort_Signal => GNARL.Undefer_Abortion; end; if not Successful then [ Parm1 := P.Parm1; ] [ Parm2 := P.Parm2; ] [ ... ] << Triggered Statements >> end if; end;
The first action made in the scope associated with the ATC is to defer the abortion8.7. Without this, an abortion that occurs between the time that the call is made and the time that the abortable part's cleanup handler is set up might miss the cleanup handler and leave the call pending). The ATC request is also handled by the GNARL procedure Task_Entry_Call but in this case the whole sequence of actions is: