From bobduff Wed Dec 16 15:56:35 1992 From: bobduff (Bob Duff) To: ada9x-mrt To: iso@ajpo.sei.cmu.edu Subject: LSN-1058 on Single Statement Restriction for ATC !topic LSN on Single Statement Restriction for ATC !key LSN-1058 on Single Statement Restriction for ATC !reference MS-9.9.4;4.6 !from Bob Duff $Date: 92/12/15 16:19:12 $ $Revision: 1.2 $ !discussion This Language Study Note discusses a restriction on the abortable part of the asynchronous select statement (ATC, for "asynchronous transfer of control"). The restriction was suggested at the November ISO/WG9 meeting in Salem. This LSN is in response to resolution number 10-7 of the Salem meeting. Given that ATC is in the language, we must work to make it as usable, and as implementable, as possible. When usability and implementability conflict, we must make sensible trade-offs. We have ensured that the following implementation strategies are feasible for ATC: - The "single thread" model, in which the task executing the asynchronous select statement executes the abortable part, and is "interrupted" if an ATC is necessary. By "interrupt" we mean some sort of asynchronous signal, such as is generally supported by Ada systems running on bare machines, and by many operating systems, such as Posix. - The "two thread" model, in which the Ada task executing the asynchronous select statement spawns a separate thread. That second thread executes the abortable part, and is aborted if an ATC is necessary. The single thread model is generally preferable, since it properly addresses the concerns of the Requirement. In particular, the overhead of creating new tasks is said to be unacceptable in the Requirement R5.3-A(1). Nevertheless, some implementers are considering the two-thread model. The two-thread model will presumably be easier to implement if the abortable part behaves more like an Ada task. For example, we have forbidden accept statements within the abortable part. This was primarily done for the semantic reasons explained in LSN-1049 (meaning of TASKING_ERROR for callers of accept statement, etc.). In addition, it might have the benefit of making the two thread model somewhat easier to implement, because the second thread will never accept entry calls on behalf of the real Ada task that created it. The one issue that came up at the Salem meeting was transfers of control out of an abortable final part. The following kinds of transfer of control are of interest here: - exit statement, - return statement, - goto statement, and - raising of an exception. (See RM83-5.1(6) for a definition of "transfer of control". The others listed there are irrelevant to this discussion.) We all agree that we do not want to ignore exceptions propagated by an abortable part. However, it has been suggested that the other forms of transfer of control should be disallowed, in order to make the abortable final part more like a task body. Note that the fact that exceptions are not ignored means that the abortable part is not exactly like a task body. That is not the goal. The goal is merely to make implementation of the two thread model relatively easier. USABILITY: This section shows that the capability of transferring control out of an abortable part has quite natural semantics, and is the obvious approach in some situations. Imagine we have a device, and we wish to write a procedure that gets the current status of the device. We tell the device to update its status register, which it is supposed to do after a while, but probably fairly quickly. We keep reading the status register (busy waiting) until it does so. If it doesn't do so within a millisecond, we reset the device, and again tell it to update its status register. The whole process repeats until we have the status, or else the number of retries is exhausted. Status_Register: Status_Type; for Status_Register'Address use ...; pragma Atomic(Status_Register); procedure Get_Status(Status: out Status_Type) is N_Tries: Natural := 0; begin Main_Loop: loop Status_Register := 0; ... -- tell device to update its status register select delay 0.001; if N_Tries >= Max_Tries then Status := Device_Not_Responding; exit Main_Loop; -- This one is OK. else N_Tries := N_Tries + 1; ... -- reset device -- and try again (i.e. Main_Loop goes around again) end if; then abort -- Busy wait for non-zero status: loop Status := Status_Register; exit Main_Loop when Status /= 0; -- Should be illegal? end loop; end select; end loop; end Get_Status; In the above example, it was natural to use an exit statement in the abortable part that exits a loop outside the abortable part. (The procedure could have been a function. In that case, the exit statement would be a return statement: "return Status;". The same issues would exist in that case as well.) If the exit statement were to be made illegal, the workaround would be as follows. Workaround lines are marked with "*" in column 1: Status_Register: Status_Type; for Status_Register'Address use ...; pragma Atomic(Status_Register); procedure Get_Status(Status: out Status_Type) is N_Tries: Natural := 0; * Done: Boolean := False; -- Set to true to exit Main_Loop. begin Main_Loop: loop Status_Register := 0; ... -- tell device to update its status register select delay 0.001; if N_Tries >= Max_Tries then Status := Device_Not_Responding; exit Main_Loop; -- This one is OK. else N_Tries := N_Tries + 1; ... -- reset device -- and try again (i.e. Main_Loop goes around again) end if; then abort -- Busy wait for non-zero status: loop Status := Status_Register; * if Status /= 0 then * Done := True; -- exit Main_Loop * exit; -- exit inner loop first * end if; end loop; end select; * exit when Done; end loop; end Get_Status; In general, we presume that a programmer might write the initial version, get an error message from the compiler, and then try the workaround, while grumbling to himself about "yet another silly error message from the &*@#! Ada compiler." The programmer would also wonder why the exit statement in the abortable part is illegal, while the one after the triggering statement is fine. The workaround forces a different style for loop exits and other control structures than is allowed in other parts of the language. WHEN IS TWO THREAD MODEL NEEDED? To put things in perspective, we should remember that the two thread model is only of interest in a situation where all of the following are true: - The Ada system is being implemented on top of an operating system or kernel of some sort, which is not subject to modification by the Ada vendor. (I.e., we're not talking about a bare machine implementation here.) - It is desirable to implement each Ada task as a separate OS thread. - The underlying OS does not support any form of asynchronous signal, other than to kill the thread outright. (I.e. we're not talking about Posix or anything else based on Unix, because those systems have asynchronous signals. VAX/VMS supports AST's.) - The underlying OS DOES support an "abort thread" primitive of some sort, which causes the targeted thread to stop running and never run again. It is necessary for this primitive to leave the data structures belonging to the thread intact, so that finalization (user-defined and task waiting) can be performed later on its behalf. Otherwise, the two thread model simply won't work. - It is important to support preemptive ATC. Ada does not in general require preemption for ATC. We intend to make it clear in the Real Time Annex that if the underlying operating system does not support asynchronous signals, then the Ada system is not required to support preemptive ATC. It is acceptable, if documented, to implement ATC by having the run-time system check "has ATC been issued" whenever it does one of the operations defined to be abort synchronization points. The programmer can insert "delay 0.0;" to force this polling to happen at a certain place. If the underlying OS doesn't support any form of asynchonous signal, one would normally not choose that OS in a situation that requires preemptive ATC. Surely, if polling is used by C programs on that system, then Ada can use the same technique. If all of the above are true, then the preemptive single thread model based on asynchronous signals is not possible, so the two thread model is necessary if preemptive ATC is required. IMPLEMENTATION OF TRANSFERS OF CONTROL IN TWO THREAD MODEL: In the two thread model, it is straighttforward to implement transfers of control from inside the second thread to outside. For exceptions, the implementation must generate an outermost exception handler for the second thread. This exception handler can record in a global variable the fact that an exception occurred, and also the identity of the exception. When the real Ada task wakes up, it notices the state of this global variable, and either re-raises the exception, or not. For exit, return, and goto statements, the implementation is similar. In fact, the implementation could model such a statement as a special exception, with a do-nothing handler placed at the target address. Whether or not the implementation does it that way, some mechanism is needed for the second thread to notify the real Ada task, so that when the latter wakes up, it knows to do a goto to a certain address. Note that for both kinds of transfers of control, and for both one and two thread models, the cancellation of the triggering statement must happen during the transfer. The transfer must wait until that cancellation is done. This is similar to task waiting -- a goto out of a block statement containing tasks must wait in "mid-air" during the jump. Any of the many implementation models already in use can achieve this. In our own analysis of likely interfaces between compiled code and the run-time system, it seems likely that for the asynchronous select statement, both the sequence of statements following the triggering statement, and the sequence of statements of the abortable part will be "bundled up" as a subprogram to ease calling them when appropriate. In this case, it is convenient to represent both sequences of statements as parameterless functions that return a code address or propagate an exception. For example, the type for these functions could be: type Seq_Of_Statements is access function return System.Address; Of course both sequences can make up-level references, so this type would have to be a local access type so displays/static links would be properly maintained. But of course the run-time system is used to playing such tricks. Given this representation, it is straightforward to replace each transfer of control with "return label'Address," possibly preceded by storing a return value into an up-level result temporary for an existing function return statement nested in one of the arms of the asynchronous select. Since this kind of transformation will be necessary for the triggering alternative, it seems no added burden to require it for the abortable part. POSSIBLE METHODS OF STATING THE RESTRICTION: If we were to go ahead with the restriction against transfers of control, we could simply forbid them (other than exceptions) if they go out of an abortable part. An alternative was suggested at the Salem meeting: require the abortable part to be a single procedure or entry call. Since exits, gotos, and returns are already forbidden to cross procedure and entry call boundaries, the desired restriction is implied. On the other hand, a statement such as "X := Function_Call(...);" seems quite sensible. Perhaps that should be allowed also. This leads us to the following restriction: The abortable part must be a single simple statement other than an exit, goto, or return statement. But that restriction doesn't sound any better than allowing many statements, but forbidding transfers of control out of the abortable part. The user would wonder why a rule that is intended to forbid certain transfers of control also forbids something like "P; Q;", which seems quite harmless. Another alternative would be to allow a single statement. The compiler could special case an exit, return, or goto statement that appears by itself -- don't create the second thread in those cases. But then the implementation is forced to do a special case check, which is what we were trying to avoid in the first place. Any of the restrictions that involve "single statement" seem like overkill, and seem non-uniform with the rest of the language. There's nowhere else where a list of statements is restricted to be of length one. (One wonders about the syntactic implications on where pragmas can appear.) Note that these restrictions are not analogous to the restriction forbidding accept statements in abortable parts. That restriction is quite naturally included with the existing restrictions that forbid nested accept statements for the same entry (family), and that forbid accept statements nested within nested program units. SUMMARY: There is potentially some minor implementation effort involved in supporting transfers of control out of abortable parts if the two thread model is used, though it is likely similar to other cases where the run-time system is provided by the generated code with a sequence of statements to be executed. There is some extra language complexity if we forbid it. We risk having one more example where the language looks silly and short-sighted by adding a restriction that is not based on solid semantic concerns. Thus, the decision boils down to a trade-off between these two issues. If the restriction is added, the rule should be that exit, return, and goto statements cannot cross the abortable-part boundary. This is the simplest rule. Programmers find rules easier to remember when they directly address the concern. If in 1999, some programmer says, "Why this rule?" and the answer is, "The designers felt that transfers of control out of an abortable part was a problem," then if the rule forbids those, it makes sense. If the rule forbids all kinds of other things as well, that programmer will laugh at us. The MRT recommendation is that we should not add such a restriction -- we believe the extra implementation effort is very small, given the need to handle exceptions anyway, and that many implementations will not choose the two thread model in any case. Creating a special case for the abortable part does not seem warranted, in that it changes the normal Ada paradigm for control flow, and inconveniences the user, with minimal benefit to the implementor. ------ Output from automsg.perl ------ Mail received from bobduff *** Key LSN-1058 given to topic 'LSN on Single Statement Restriction for ATC' ----------- End of output ------------