Note that if the exception were propagated to the parent task, it would mean that child tasks could interfere asynchronously with their parent, and it would also mean that these interferences could occur simultaneously, with disastrous results.
We will now consider two other interactions between tasking and exception handling:
|In this section...|
procedure PERFORM is task A; task B; task C; ... begin -- activation of A, B, C, in parallel -- (1) ... exception when TASKING_ERROR => ... -- (2) end;
The semantics of Ada specifies that the three local tasks are activated, in parallel, after begin but before the first statement of the procedure. This means that at (1) we can rely on the fact that all three tasks are activated.
Consider now what happens if the activation of one (or more) of these tasks is not started as a consequence of the raising of an exception. It would not make much sense to execute the statements of the procedure, once it is known that one of the basic preconditions for its proper operation is not satisfied. For this reason, the execution of statements of the procedure is not started, and the predefined exception TASKING_ERROR is propagated at (1) to be handled by the exception handler at (2).
(By analogy, if A, B, and C were array declarations, the statements of the procedure would not be executed if the elaboration of any of these declarations raised an exception. The analogy stops there however, since in the case of task activation the exception is raised in the statements, and can be handled locally, whereas in the case of arrays the exception is raised in the declarative part and propagated to the caller to be dealt with. Activation of a task behaves like an implicit initialization statement - placed after begin.)
Note that the exception that is propagated at (1) does not depend on what caused the abandonment of task activation. What matters for the procedure is to know whether or not activations have succeeded. Should one or more of them have failed, it does not matter much whether this is by constraint violation, or by a numeric error: in any case some other treatment is needed. This therefore is the justification for the propagation of the less specific exception TASKING_ERROR. By the same reasoning, it does not matter much whether one, or more than one, task failed to be activated. Hence a single exception is raised in either case.
As a basis for discussing the various cases that may arise, consider a task SERVER that provides the entry UPDATE:
task SERVER is entry UPDATE(THIS : in out ITEM); end; task body SERVER is ... begin ... accept UPDATE(THIS : in out ITEM) do -- statements for servicing the request end UPDATE; ... end SERVER;
and another task called USER, having no entry at all:
task USER; task body USER is THING : ITEM; begin ... SERVER.UPDATE(THIS => THING); ... end USER;
The first interaction to consider is what happens if the USER calls an entry of the SERVER
SERVER.UPDATE(THIS => THING);
at a time when this called task has already completed its execution. Clearly the called task will never accept the entry call, and hence there is no point in letting USER (the caller) wait forever. Consequently the exception TASKING_ERROR is propagated to the caller at the point of call: the caller is thereby informed that the call cannot be accepted. For similar reasons, this exception is also raised if the called task (SERVER) has not already completed its execution, but proceeds to do so without encountering an accept statement for the entry call.
abort SERVER; -- in the second case abort USER; -- in the third case
Such a statement, to be used only in extreme circumstances, will eventually cause completion of the aborted task; this will in any case occur no later than when the aborted task reaches a synchronization point: a point where it causes the activation of another task; an entry call; the start or the end of an accept statement; a select statement; a delay statement; an exception handler; or an abort statement.
We next analyze the consequences of each of these possible abnormal situations, both with respect to the task issuing the entry call (USER) and with respect to the task containing the accept statement (SERVER).
Consider a situation in which an exception, say error, is raised within the accept statement of SERVER:
task body USER is ... SERVER.UPDATE( THIS => SOME_ITEM); ... ... ... end USER;
task body SERVER is ... accept UPDATE(THIS : out ITEM) do error end; ... end SERVER;
From the point of view of the caller, the accept statement is analogous to a procedure body that is executed when the corresponding entry is called. Hence if an exception is raised (and not handled within the accept statement itself) it should be propagated at the point of call of SERVER.UPDATE.
However, from the point of view of the task that contains the accept statement, this statement is a normal statement of its body. Hence if an exception is raised within SERVER, it should be handled by a handler provided within that same task (outside the accept statement).
To summarize, an exception raised within an accept statement (and not handled there) is propagated both in the calling and in the called tasks. Both tasks may provide handlers for the exception.
A different treatment must be employed if the called task (SERVER) is aborted by a third task. In this case the caller (USER) must be informed that the entry call will never be completed: For this reason, the exception TASKING_ERROR is propagated at the point of the entry call.
In this case, the called task (SERVER) completes the rendezvous normally: the called task is unaffected.
There are good reasons for this dissymmetry of treatment. First, we can expect servers to be programmed in a more robust manner than user tasks. Moreover it is important to ensure continuity of service, and this would not be the case if it were possible for a single unsound user to affect the service to all user tasks. In terms of the implementation, this means that the storage of an aborted task cannot be reclaimed before the end of the rendezvous: this is important if the entry has in out or out parameters that are implemented by copy, or parameters of any mode that are implemented by reference.
task SPOOLER is entry START_TRANSFER(SOURCE, DESTINATION : in STRING); end; task body SPOOLER is INPUT : FILE_TYPE; OUTPUT : FILE_TYPE; C : CHARACTER; procedure OPEN(SOURCE, DESTINATION : in STRING) is begin OPEN(INPUT, MODE => IN_FILE, NAME => SOURCE); begin OPEN(OUTPUT, MODE => OUT_FILE, NAME => DESTINATION); exception when NAME_ERROR => -- also propagated to calling task CLOSE(INPUT); raise; end; exception when NAME_ERROR => raise; end; begin loop begin accept START_TRANSFER(SOURCE, DESTINATION : in STRING) do OPEN(SOURCE, DESTINATION); end; loop GET (INPUT, C); PUT (OUTPUT, C); end loop; exception when END_ERROR => -- handled locally and not propagated CLOSE(INPUT); CLOSE(OUTPUT); when NAME_ERROR => null; -- restart main loop end; -- the calling task has also received this end loop; end SPOOLER;
Two forms of input-output exceptions may be raised within the body of the task SPOOLER. The exception END_ERROR is handled locally and not propagated. The exception NAME_ERROR may be raised within the accept statement for the entry START_TRANSFER. The handler provided within SPOOLER simply prepares it for another iteration. In addition, the occurrence of this second exception also has an effect on the calling task USER. The exception is propagated in that task where it can be serviced by a local handler:
task body USER is OLD_FILE : constant STRING := ">UDD>PROJECT>JAN"; NEW_FILE : constant STRING := ">UDD>PROJECT>FEB"; begin ... begin SPOOLER.START_TRANSFER(SOURCE => OLD_FILE, DESTINATION => NEW_FILE); exception when NAME_ERROR => -- do something on OLD_FILE and NEW_FILE end; ... end;