!topic LSN on Asynchronous Transfer of Control !from Tucker Taft 92-02-05 !discussion Chris Anderson has asked us to prepare LSNs on Asynchronous Transfer of Control and Multi-Way Select to help in evaluating how and whether the related revision requirements should be supported in Ada 9X. This LSN discusses the Asynchronous Transfer of Control (ATC) proposal from MS 4.0, and considers some simplifications and alternatives. Currently ATC is coupled quite strongly with multi-way select. For the purposes of this LSN, we will try to separate the two. Therefore, we first summarize a "stand-alone" ATC proposal (please read upper case as "bold"): --------------------------------------- atc_statement ::= SELECT atc_event_alternative THEN ABORT sequence_of_statements END SELECT; atc_event_alternative ::= delay_statement [sequence_of_statements] | entry_call [sequence_of_statements] The expressions in the delay statement or entry call are first evaluated. In the case of a delay alternative, if the delay amount is less than or equal to zero, the atc event alternative is immediately selected. In the case of an entry call alternative, if the protected record entry barrier is true, or the task entry is open, then the atc event alternative is immediately selected. If the event alternative is not immediately selected, the (abortable) sequence of statements following THEN ABORT is executed pending the selection of the event alternative. If the abortable sequence of statements completes prior to selection of the event alternative, the delay or entry call is cancelled. If the event alternative is selected prior to the completion of the abortable sequence of statements, the abortable sequence of statements is aborted sometime prior to execution of the sequence of statements of the event alternative. The sequence of statements of the event alternative is executed after the entry call completes (for an entry call alternative) and the aborted sequence of statements completes. ----------------- This ATC construct provides a general mechanism to stop a task from what it is doing and redirect it to a new sequence of statements. The triggering event can be the expiration of a delay, or the acceptance of an entry call. When coupled with a protected record (see the example of Mode change from MS version 3.1, in section 9.9.3), multiple tasks can be affected by a single mode change event or interrupt. Here are other examples of use, borrowed from MS version 4.0: -- Example of use for an interactive application: loop select Terminal.Wait_For_Interrupt; Put_Line("Interrupted"); then abort -- This will be abandoned upon terminal interrupt Put_Line("-> "); Get_Line(Command, Last); Process_Command(Command(1..Last)); end select; end loop; -- Example of use for a time-limited calculation: select delay 5.0; Put_Line("Calculation does not converge"); then abort Complicated_Recursive_Function(X, Y); end select; Here is part of the example from MS version 3.1 for those who have lost that document: -- Adapt to globally set mode loop select Mode_Manager.Wait_For_Mode_Change(My_Mode)(New_Global_Mode); -- Mode changed, set new local mode and loop around My_Mode := New_Global_Mode; then abort case My_Mode is when Normal => ... -- Do normal mode processing when Mode_2 => ... -- Do mode 2 processing when Mode_3 => ... -- Do mode 3 processing . . . end case; end select; end loop; ---------------------------------- First, let's look at alternatives using the abort statement: -- Interactive program loop declare task Killer; task Interpreter; task body Killer is begin Terminal.Wait_For_Interrupt; abort Interpreter; -- Kill off the competition Put_Line("Interrupted"); end Killer; task body Interpreter is begin -- This will be abandoned upon terminal interrupt Put_Line("-> "); Get_Line(Command, Last); Process_Command(Command(1..Last)); abort Killer; -- Kill off the killer end Interpreter; begin null; -- Wait for tasks to complete normally or be aborted end; end loop; This same structure can be replicated for all three examples. Note that the first two examples had nothing particular to do with tasking originally. When forced to use abort, both must create 2 tasks, abort 1, and allow another to complete normally. In some special circumstances there may be ways to avoid having to spawn and abort the "Killer" task each time. ----------------------------------- This structure can be approximated in a generic as follows: generic with procedure Event; with procedure Abortable_Operation; package Asynchronous_Transfer_Of_Control is function Event_Occurred return Boolean; -- This function calls the Abortable_Operation procedure -- while also waiting for the Event procedure to return. -- Whichever returns first aborts the other. -- True is returned if the Event procedure returned before -- it got aborted. end Asynchronous_Transfer_Of_Control; The example becomes: -- Interactive program declare procedure Interpreter is -- This will be abandoned upon terminal interrupt Put_Line("-> "); Get_Line(Command, Last); Process_Command(Command(1..Last)); end Interpreter; package Interp is new Asynchronous_Transfer_Of_Control( Event => Terminal.Wait_For_Interrupt, -- This works because the entry is parameterless -- If there were parameters, we would have to declare -- a local procedure which called the entry, and pass it in. Abortable_Operation => Interpreter ); begin loop if Interp.Event_Occurred then -- This invokes the Interpreter while also listening for event Put_Line("Interrupted"); end if; end loop; end; end loop; The generic could be implemented as follows: package body Asynchronous_Transfer_Of_Control is function Event_Occurred return Boolean is Result : Boolean := False; begin declare task Event_Task; task Abortable_Operation_Task; task body Event_Task is begin Event; abort Abortable_Operation_Task; -- Kill off the competition Result := True; -- hope this doesn't get aborted in the middle end Event_Task; task body Abortable_Operation_Task is begin -- This will be aborted if event procedure returns Abortable_Operation; abort Event_Task; -- Kill off the killer end Abortable_Operation_Task; begin null; -- Wait for tasks to complete normally or be aborted end; return Result; -- Indicate whether event occurred end Event_Occurred; end Asynchronous_Transfer_Of_Control; The intent of making this a generic is to allow an implementation to "simulate" the effect of spawning and terminating two tasks without really doing so. Unfortunately, it may be difficult (or undesirable) to exactly mimic the semantics implied by the body given above, so the specification for the generic will want to be loose enough to allow appropriate near equivalents. -------------------------------------- A more specialized generic could be created to handle the timed case, taking a duration or time rather than the more general "Event" procedure, with a spec as follows: generic Abort_In : DURATION; -- or Abort_At : MONOTONIC.TIME; -- or Abort_At : CALENDAR.TIME; with procedure Abortable_Operation; package Time_Limited_Operation is function Timer_Expired return Boolean; -- This function calls the Abortable_Operation procedure -- while also waiting for the delay to expire. -- Whichever finishes first aborts the other. -- True is returned if the Timer expires before -- the Abortable operation completes. end Time_Limited_Operation; Here is a use of this more specialized generic for the timed calculation: declare procedure Calculate is begin Complicated_Recursive_Function(X, Y); end Calculate; package Timed_Calculation is new Time_Limited_Operation( Abort_In => 5.0, Abortable_Operation => Calculate ); begin if Timed_Calculation.Timer_Expired then Put_Line("Calculation does not converge"); end if; end; --------------------------------- At this point it is worth mentioning that CIFO 3.0 also includes a generics-based proposal for asynchronous transfer of control (see Ada Letters XI.8 Fall 1991 pp. 8-2 to 8-6). The CIFO 3.0 approach is more closely linked with the selective wait (accept) statement than those given above. --------------------------------- Given the above explanation, we need to decide on the following