Ada 9X Language Study Note LSN-042-DR Tasks and ProtectedRecords P. N. Hilfinger Version 4.0, 19 March 1992 1 Introduction The MRT has proposed a new construct, the protected record, to capture the concept of a data object to which concurrent access is controlled. In Ada 83 (as in CSP and Occam), this function was intended to be performed by the same mechanism that handles intertask communication, the rendezvous. It appears that this philosophy has in fact never been shared by much of the real-time community, nor has it (at least until recently) been supported properly by compiler vendors. Consequently, there are both practical and technical reasons for making some distinction between the two concepts of protected object and more general task. Nevertheless, as I shall argue here, the semantic similarities between the two concepts are very strong, and the MRT proposal, in its current form, fails to take proper advantage of this fact. By seeking a more unified solution, we can achieve the desired separation of protected object and task, with its attendant advantages in simplifying implementation, while at the same time avoiding ``reference manual bloat''---the introduction of a plethora of new semantics. At the same time, I believe that we can accelerate the transition to Ada 9X by allowing a partial realization of protected record features in Ada 83 implementations. The MRT has offered a comparison of protected records and ``passive'' tasks in an attempt to show that no such unification is desirable. However, the precise ``passive task'' formulation that they are using for comparison is never specified, so that unfortunately their discussion of this point is wasted. I certainly do not intend to argue here that the mere addition of a pragma to Ada 83 tasking is going to fulfill the requirements; indeed I think that many if not most of their proposals in this area ought to be adopted. My only goal is to seek a smoother transition from Ada 83 and to avoid semantic redundancy. The actual proposals are in section 3. I'll begin with some comparisons between protected operations and rendezvous. 2 Basic Procedure and Entry Calls Let's start by comparing the basic features of entry and procedure calls in protected records with those of entry calls in rendezvous. Since my ultimate purpose is to consider alternate ways of embodying 1 the functionality and performance afforded by protected records, I will assume throughout, unless otherwise stated, that other proposed revisions---especially those involving priorities---are in place. Consider first the model protected record given in Figure 1 and the Ada 83 task type given in Figure 2. The MODEL_RECORD type illustrates procedures and entries in protected records. The first question to address is in what ways the semantics of MODEL_RECORD differs from that of MODEL_TASK. 2.1 Allocation and Initiation At the time the protected type declaration is processed, the compiler determines the size of each MODEL_RECORD. This size is not necessarily a compile-time constant, since the size of STATE_DATA need not be a compile-time constant. However, it is a constant at the time the declaration is elaborated. The compiler also determines the initialization routine needed to allocate a MODEL_RECORD. Not all MODEL_RECORDs need have the same initial value, since not all DATUMs need to have the same initial value. However, if the initial value of STATE_DATA happens to be a constant (as it most often is), the compiler can determine this at the time it processes the declaration. Any initialization of MODEL_RECORDs is done by the instantiator. By contrast, the size of a MODEL_TASK becomes known in general only at the time the declarations in its body are elaborated. Since N may change after each instantiation of a MODEL_TASK, each task of this type may have a different size. The allocation and initialization of the state variable S may usually be carried out by the instantiator, except in the unusual (and difficult-to-detect) circumstance that initiation of S causes blocking or looping. One important contrast is that protected records are supposed to be available for use immediately upon allocation---callers do not block or contend, waiting for task initiation. It is an error for them to call before elaboration of the protected body, while in tasks, this simply causes blocking. I have no clear feeling for what difference this makes in practice. It suggests the advisability of allowing (but not requiring) declared Ada task objects in general to be activated at any time that is after both the appearance of their declaration and of their body. 2.2 E0, E1, and P0 calls in MODEL_RECORD Calls on the protected record entries and procedures proceed according to one of the following. 2 protected type MODEL_RECORD is procedure P0(X: DATA); entry E0(Y: DATA); entry E1(Z: DATA); record S: STATE_DATA(N); -- N is some global variable. end MODEL_RECORD; protected body MODEL_RECORD is procedure P0(X: DATA); begin UPDATE0(X, S); end; entry E0(Y: DATA) when TRUE is begin UPDATE1(Y, S); end; entry E1(Z: DATA) when OK(S) is begin UPDATE2(Z, S); end; end MODEL_RECORD; Figure 1: A model protected record. 3 task type MODEL_TASK is entry P0(X: DATA); entry E0(Y: DATA); entry E1(Z: DATA); end MODEL_TASK; task body MODEL_TASK is S: STATE_DATA(N); -- N is some global variable. procedure P0_B(X: DATA); begin UPDATE0(X, S); end; procedure E0_B(Y: DATA) begin UPDATE1(Y, S); end; procedure E1_B(Z: DATA) begin UPDATE2(Z, S); end; begin loop select accept P0(X: DATA) do P0_B(X); end; or accept E0_B(Y: DATA) do E0_B(Y); end; or when OK(S) => accept E1_B(Z: DATA) do E1_B(Z); end; or terminate; end select; end loop; end MODEL_TASK; Figure 2: MODEL_RECORD redone as a task (no exception handling). 4 1.The caller ``contends'' for the MODEL_RECORD. Because of the priority assigned to a protected record and the rule that only tasks with that priority or less may call it, this ends up meaning the following. -- On a uniprocessor, the caller proceeds immediately; contention is a null operation. -- On a multiprocessor, callers queue chaotically; the order in which processes obtain exclusive access to the MODEL_RECORD is not specified, and starvation is possible in principle. 2.The caller's priority is raised to that of the MODEL_RECORD. If time-slicing among processes of equal priority is in effect, it is suppressed. Certain interrupts associated with the entry or procedure call are masked off (how is the subject of other MRT proposals). The net effect is that the caller cannot be preempted by other processes that may properly call an entry of the MODEL_RECORD. 3.When holding the MODEL_RECORD, the caller evaluates any ``barrier condition.'' Procedures never have a barrier condition. If there is a false condition, the caller enqueues itself in some order on a queue for the given entry. The phrase ``in some order'' merely indicates that the issue of queuing on entry calls of tasks and protected records is handled by other MRT's proposals. 4.In the case the barrier condition is true or non-existent, the body of the procedure or entry is executed. 5.If there is a false barrier condition, the process suspends itself, releasing its hold on the MODEL_RECORD, and undoing changes to its priority, etc. 6.The implementation has (at least) two choices at this point, When a process suspended on an entry queue is released: 6A. Its priority and preemptability are reset as in step 2. It then executes the body of the entry. 6B. When done with its entry or procedure body, a process evaluates barrier conditions on non-empty entry queues. If it finds a true one, it releases the next process on that queue, which then becomes the holder of the MODEL_RECORD. 6C. If it has not relinquished its hold on the MODEL_RECORD as a result of step B, the process does so now. Until this process, or the one to which it gave control in step B, releases the MODEL_RECORD, no other contenders may obtain hold of it or enqueue on any of its entry queues. 5 Alternatively, a process that did suspend simply waits to be released and then continues with the next statement after the call; and if the process did not suspend on an entry queue, then when it is finished with executing its body, 6B'. It evaluates conditions on non-empty entry queues. If it finds a true one, it executes the appropriate body on behalf of the enqueued process, releases that process, and repeats step 6B'. 6C'. The process then relinquishes its hold on the MODEL_RECORD and undoes changes to priority and preemptability. 2.3 Aside: Do E0 and P0 differ? In this example, I find it hard to escape the question of whether there is really any difference between the processing of calls on E0 and P0 in MODEL_RECORD. The MRT proposal gives them two entirely different constructs, and the MRT has insisted that there is an essential difference. However, as the discussion in section 2.2 makes clear, there is no semantic difference. As for implementation, it is likely that there is no difference here either. Both of these are likely to be implemented as subprograms that differ only in that one contains a piece of code whose effect is if not TRUE then RELEASE_LOCK_AND_SUSPEND_SELF; ... or in other words, NOP. The calling conventions of the two could be identical. Of course, in a protected record implementation, it is visible that a certain call will suspend only for an incorrect program---in particular, there is no guard (barrier) and there are is not supposed to be any suspension during execution of the body. However, the importance of having a piece of the language syntax indicate this is questionable, especially since compilers cannot provide complete static checking that no attempted suspension will occur. That is, if a programmer sees entry AWAIT_SAFETY; -- Wait for absence of attack animals (potentially blocking). entry FOO; -- Release the tiger (non-blocking). (i.e., with the presence of guards indicated by comments instead of syntax) rather than 6 entry AWAIT_SAFETY; -- Wait for absence of attack animals. procedure FOO; -- Release the tiger. is he any more likely to commit the error of calling AWAIT_SAFETY from within a protected operation? Furthermore, are such incorrect calls of AWAIT_SAFETY common enough that static checking is at all important? There are restrictions on how procedures may be used. They may not be the subject of requeue and they may not be used as select alternatives. These restrictions have some slight benefit in space efficiency; an obvious way to deal with the fact that entries can appear in all these contexts is to have several entry points at fixed positions in the code generated for each entry---one for ``plain'' entry calls, one for timed entry calls, etc. These extra entry points would be avoided for procedures. Also, a compiler can determine from a protected_specification alone that a procedure needs no space for a queue. However, these are not advantages in speed, they are not semantic distinctions in functionality, and even the space advantage seems awfully small to justify adding a new construct. 2.4 Comparison with entry calls on MODEL_TASK: uniprocessor Priorities. The priority rules applied to MODEL_RECORD may also be applied to MODEL_TASK; the only inconsistencies with Ada 83 are first that high-priority tasks may call low-priority tasks, and second that Ada 83 tasks may not have interrupt-level priorities. The second difference was never defensible in Ada 83 anyway, and is well changed. The first difference may not really be a difference, since there are precedents for pragmas that render certain programs erroneous. Assuming then that MODEL_TASK is subject to the same priority rules as MODEL_RECORD, plain old Ada 83 semantics allow calls on entries of MODEL_TASK to follow steps 1 and 2 of section 2.2. Barrier condition evaluation. The evaluation of the barrier conditions vs. evaluation of guards is interesting. The mapping team had originally applied a classical monitor restriction to the barrier conditions: the only mutable state to which they were allowed access was that of the protected record. For some reason (possibly a misunderstanding of one of my own comments, I regret to say) this rule was relaxed in version 2 of the mapping document. The restriction should be re-instated; it's a good idea (it need not be statically enforced; erroneousness is good enough). If only the state of the record is accessible, then it is semantically transparent (infinite 7 loops aside, as usual) just which process evaluates the guards and when. Thus, barrier conditions may be evaluated by callers, or they may be evaluated on exit from a protected record (leaving ``entry open'' bits around, as in some Ada implementations). Also, there is no question of whether barriers become true asynchronously when global state changes. In one of Offer's notes (18 January 1991), he indicated that ``Our current thinking is [that referencing] non-constant objects in the barrier expression is either meaningless or erroneous.'' I'll go along with that. With this restriction, current Ada 83 semantics indicates that the behavior expected of calls on MODEL_RECORD is generally acceptable for MODEL_TASK. That is, the implementations sketched in section 2.2 works for MODEL_TASK, assuming the objects of these types are used the same way. Going from ``generally acceptable'' to ``acceptable'' requires only a very subtle change from Ada 83. Under current Ada 83 rules, it seems that all guards must be evaluated on encountering a select, even after one is found to be true on a non-empty queue. Under the MRT proposal, which ``open'' barrier conditions get evaluated is non-deterministic. Since it is extremely doubtful that programmers would ever depend on all guards being evaluated (side-effects in guards being in bad taste), changing current Ada semantics to allow early abandonment of guard evaluation is perfectly reasonable. Restrictions on operations. Under Ada 83 rules, the MODEL_TASK would normally be allowed certain actions forbidden in MODEL_RECORD: delays, and activities that cause suspension on an entry queue. Static detection of these forbidden activities is in general difficult, and the separation of protected records from tasks does not seem to make enforcement any easier. Self-calls. One other difference is that the MRT proposal would allow, e.g., E0 to call P0 without deadlock (since no lock is re-acquired). This is a definite difference and is occasionally convenient. On the other hand, the work-around is rather obvious (for example, in Figure 2, E0_B may call P0_B.) Also, I wonder if the MRT has considered the small but perhaps annoying distributed overhead this functionality seems to incur. A return from P0 generally entails looking for an enqueued process to release. When called from within another procedure or entry, however, this action must be suppressed. Thus, there is presumably one more run-time test in the body of P0, and (looking ahead for a moment to multiprocessors) there is a test on attempting to acquire a lock to see whether the current process already holds it. Exceptions. The only essential difference I find is in the treatment of exceptions. According to the Mapping Document, an exception in 8 evaluating a barrier condition in a protected record propagates to all tasks enqueued on that entry. This is sensible behavior; since barrier conditions shouldn't (as a matter of taste) have side-effects, if their evaluation yields an exception once, it will do so again, and there is no point in re-evaluating it. On the other hand, under the same assumptions, it probably wouldn't change much to allow the barrier exception to be re-evaluated for each task on the queue. In a task, such an exception would be handled by the task containing the called entry. In the case of MODEL_TASK above, the exception would be uncaught, causing all callers to receive TASKING_ERROR at that point and forever after---clearly undesirable behavior. On the other hand, the MRT proposal is not a perfect solution. In general, the barrier conditions should depend only on internal state of the protected record, and have no side-effects. Only the routines in the protected record can access this state; thus, only routines in the protected record can recover from the situation that caused the exception in the first place. It is therefore not exactly clear how the protected record writer should provide for exception recovery from barrier condition failures. That is, when a caller receives an exception, what does it do? Call a ``reset'' routine in the protected record to clean up? If so, the writer of the reset routine will have to be careful, since there might be any number of callers that receive the same exception (and thus call the reset routine, causing multiple resets). It almost looks as if one wants to have an exception handler in the protected body to handle such conditions, and to propagate the exception to callers only as a last resort. For example protected body MODEL_RECORD is ... exception when ERROR_FROM_OK => CLEAN_UP(S); end MODEL_RECORD; with the intended semantics that if this handler would catch exceptions propagating out of barrier condition evaluations and then cause re-evaluation the barrier conditions. In the absence of such a handler, an exception in a barrier condition would be a serious error, possibly causing TASKING_ERROR to all and sundry, or possibly just causing erroneousness. Such a semantics, however, would move the handling of exceptions in protected records rather close to those for tasks; for example, MODEL_TASK above would have body like this: 9 task body MODEL_TASK is ... loop begin select ... end select; exception when ERROR_FROM_OK => CLEAN_UP(S); end; end loop; end MODEL_TASK; Ted Baker has pointed out to me the possibility of the above code causing an infinite loop (furthermore, an infinite loop in which the task accepts no further calls). This is true, of course, but it seems that in the analogous protected record, one has the callers write something like the following to be defensive: loop begin ... PR.ENTRY_CAUSING_BLOW_UP; ... exception when ERROR_FROM_OK => PR.CLEAN_UP; end; end loop; Now this loop may allow fairer progress in the rest of the system, but the program is still just as wrong, and what's worse, invocation of the recovery action is spread all over. In short the handling of exceptions in barrier conditions is essentially different from the handling of exceptions in entry guards. However, the particular choice of semantics for protected records raises some potentially sticky questions for the programmer. One way of addressing these questions leads to something that looks rather more like what one might do in an Ada 83 task. However, this remains an open issue. 2.5 Additional considerations for multiprocessors The issue raised by multiprocessors---which in this context mean shared-memory multiprocessors---is in the semantics of contention, described in step 1 in section 2.2. In a multiprocessor, it is possible for several tasks to reach an entry call simultaneously. Furthermore, a process can reach an entry call on a task in rendezvous, even when it has a priority lower than or equal to that of 10 the task. None of these are possible on a uniprocessor for calls on a task that obeys the protected record restrictions and priority rules. When such situations occur, Ada 83 would first require FIFO queuing of the calls on their respective entries, whereas the MRT proposal allows chaotic queuing on the entries (or procedures). Second, Ada 83 forbids deadlock in this situation; a calling process that finds its entry closed must relinquish control of the processor to an eligible process. It is envisioned that a protected record, on the other hand, be protectable with a spin-lock. Interestingly, it has been argued that Ada 83 implementations may already support spin-lock semantics. We can imagine that a task approaching an entry call ``senses'' that the task is already held and slows down until the task is no longer held. This interpretation has been used to explain why Ada 83 really does support priority-order queuing, but is nevertheless rather controversial in that context. In particular, it would seem that AI-325 considerations outlaw it, even if the reference manual does not. For the present argument, however, the point is that the use of busy waiting is not a large departure from current Ada 83 semantics. 2.6 Summary of basic procedure and entry calls It appears that the proposed semantics of protected records are nearly isomorphic to a particular allowed semantics of certain Ada 83 tasks that observe corresponding restrictions on their guards and bodies. It follows that if these tasks can be properly distinguished, the implementation (and most importantly, the performance) of the resulting task implementations can be identical to that of protected records. So far as procedures and entry calls go, there is nothing to distinguish protected records from tasks, protected record entries from task entries, protected record procedures from task entries without guards, or barrier conditions from guards. 3 Mapping Protected Records to Tasks. There seems to be general agreement that the functionality provided by the MRT protected record proposal is desirable. To the extent that Ada 83 supports this functionality, it either does so clumsily, or requires undue (or unachievable) sophistication in compilers to produce the required performance. The issue is whether correcting these faults requires an entirely new language construct, especially if its abstract semantics seem in places so nearly indistinguishable from tasks. It would help in resolving this issue to have a proposal in which the desired functionality is embodied as a modification of Ada 83 11 tasking. The MRT has produced considerable criticism of ``passive tasks.'' There is no ``passive task'' proposal, however, so the validity and significance of this criticism is highly questionable; the MRT is free to (and has in fact) ascribed arbitrarily nasty characteristics to ``passive tasks'' and no refutation is possible. In this section, therefore, I will put forward a specific proposal for integrating the functionality. Since ``passive tasks,'' whatever they might be, have already been discredited, I propose the term ``monitor tasks'' for that portion of this proposal that covers the functionality of protected records. 3.1 Layer I: Monitors Arguably, the biggest problem with Ada tasking when it comes to implementing processless objects is one of performance. The restrictions on protected records as compared to tasks were chosen to allow for efficient implementations without compile-time analysis. Basically these same restrictions must be applied to tasks to make them suitable. The pragma MONITOR must appear immediately after the is of a task_specification (as a sort of extension of the header). It marks the containing type as a monitor type (whose instances are monitor tasks or monitors). A monitor task A.requires no separate thread of control; all of its actions are to be executed by its callers. In particular, its guards and any code outside accept bodies are executed by its callers. B.has a priority; if not given explicitly, it defaults to the highest non-interrupt priority. C.(at least) in real-time implementations, is to be implemented according to a specific model given in the real-time annex. Monitor tasks must obey certain restrictions whose violation causes a bounded error, as follows. 1.The evaluation of guards in monitor tasks may involve only variables local to the task and the control variable of the discrete_range (if any). 2.No entry or function of a monitor task will be called from a task with higher priority (else PROGRAM_ERROR). 3.A monitor task will never execute a potentially-blocking operation other than an accept or a select containing only accept and terminate alternatives. 12 4.No select or accept statement in a monitor task occurs nested in a block_-statement with a declarative part, or in an accept statement. 5.A monitor task is never aborted. It is never referenced after it is terminated. 6.All selective_waits in a monitor task include a terminate alternative. Of these restrictions, items 4--6 are restrictions peculiar to an Ada-task formulation; items 1--3 are taken from the protected record proposal. Thus, in comparing proposals, the addition of protected records should be compared with the addition of items 4--6 and the MONITOR pragma only, as 1--3 are common to both. 3.2 Discussion of the monitor extension The use of a pragma extension to Ada 83 has the advantage that the semantics of guards and rendezvous are already stated. It is indicative of the closeness of protected record and task semantics that the priority rules in Ada 83, coupled with the treatment of priority for monitor tasks (stolen from the MRT), automatically guarantee that a calling task may execute the code in the monitor task outside the rendezvous, even if that code loops. Restrictions on monitors. The point of restriction 5 on abortion and reference after termination is to avoid the need to check that a monitor task is complete. The restriction on abortion is clearly desirable, because providing for asynchronous termination involves expense that is generally unnecessary for non-processes. The restriction on access after termination is less important, although it does avoid having to keep around (and check) state indicating whether a monitor is terminated. One could argue that if something goes wrong with a monitor (an exception propagates out, for example) it would be rather nice for subsequent callers to get TASKING_ERROR or some other indication of disaster (as they would with an ordinary task) as opposed to (probably) deadlocking at the call. The overhead of removing this restriction on accessing terminated tasks is probably small enough to be worthwhile. The point of restriction 6 (requiring that monitor tasks always be waiting to terminate) is to avoid the overheads associated with keeping track of tasks that must be awaited before block exit. As suggested by property C, not all Ada implementations will need to have a special implementation for monitor tasks. For example, the use of spin-locks, the strategy of keeping the TCB locked while the 13 monitor task is active, and the distinction between contention and suspension are not usually functionally important (indeed, in some applications they are downright undesirable), but are potentially important if one has a real-time application in which reliable timing analysis is necessary. Data layout and initialization. One natural result of using the currently-proposed protected-record feature is that the data needed to represent a given protected-record type is derivable from specification part alone. This requirement gives two clear advantages to the protected record's inclusion of local data in the specification: first, all instances of a protected record without discriminants are known to be of the same size, and second, if all components have static subtypes, this size and all required initialization are known to the compiler as soon as the protected record's specification is processed. In contrast, allocation and initialization for monitor task bodies would be, like all other task bodies, ``off-line.'' That is, the representation for a monitor task would consist of two parts: a specification part containing entry queues, locks, and other items deducible from the task_specification; and a body part containing the local data declared in the task_body. The specification part would contain a pointer to the body part. On closer scrutiny, however, the elaboration-time advantages to be gained by the protected record's arrangement seem to be marginal in most cases. To see this, it suffices to consider the case in which the local data of a monitor task has a static size and initialization, since this is the case in which initialization of the corresponding protected record is cheapest, and its advantage in overhead costs is therefore greatest. Consider first the case of an object (monitor task or protected record) that is local to a subprogram. Elaboration of a protected record object declaration in these cases involves 1.Modify the stack pointer by a static amount to allocate space for the protected record. 2.Move the initial data (of known size and content) into the protected record. By contrast, a corresponding task would do the following steps. 1'.Modify the stack pointer by a static amount to allocate space for the task_specification part (entry queues, etc.). 2'.Move the initial contents of the task_specification part into the area allocated. 14 3'.Jump to a thunk for the rest of the initialization (not a general subprogram linkage, since the stack will have to be manipulated). 4'.(In the thunk) modify the stack pointer by a static amount to allocate the data portion of the task. 5'.Store the address of the allocated portion in the task_specification part. 6'.Move the initial data (of known size and content) into the local data portion. 7'.Return from the thunk. Items 3'--5' and 7' constitute the additional overhead incurred by the monitor task (item 6' corresponds to part of item 2). I am assuming a straightforward compiler in which the task_body is compiled after the code that allocates the task object, so that thunks are basically unavoidable. With the understanding that instruction count estimates are always dangerous, we can at least get some idea of the magnitude of the additional overhead. On a 68000, the overhead items seem to work out to about six instructions (load the task_specification address, branch to subroutine, pop return address to register, subtract from stack register, store stack register, jump through register). Let us say 10 instructions. For a pre-elaborated monitor task (presumably at library level), all of this overhead vanishes. If a compiler compiles the task body before any use of it, it could likewise remove the overhead in principle. Let us, however, concentrate on the worst case and try to evaluate this 10-instruction cost to see how important it is to remove. The cost is invoked only at elaboration time. For a 1-MIP machine (whatever that means), that means that each 100 monitor tasks declared add 1 msec of overhead time to elaboration, which generally means to the cost of initialization, restart, and major mode change. It would be interesting to look at some real systems and see whether costs of this magnitude would be significant. On the basis of back-of-the-envelope sorts of figures that I've heard tossed around informally, they would not. Given that in real-time programs, most of these monitor tasks are likely to be pre-elaborated, the allocation costs seem even less important. Splitting a monitor task in two requires that each rendezvous establish addressability of the local data. However, this should generally require only a single instruction. In summary, the use of monitor tasks as opposed to protected records requires both either out-of-line allocation and out-of-line initialization. There is no reason to believe, however, that the executions costs of either of these is significant. 15 Rendezvous overheads. In compiling an entry body of a monitor task, it is convenient to know that the entry appears in all select statements. Otherwise, there is an additional condition on the guard, meaning roughly ``and the task is waiting at a select that mentions this entry.'' This could be fixed easily with an additional restriction. However, in the absence of any evidence that the extra cost is significant (for one thing, compilers can certainly notice when such a test is redundant), I see no need for such a restriction 3.3 Layer II: Entry bodies The idea of making accept bodies more subprogram-like is a nice notational convenience, applicable to tasks as well as protected records. We allow an entry_body as a kind of proper_body (section 3.9). proper_body ::= subprogram_body | entry_body | package_body | task_body An entry body is as in the MRT proposal. entry_body ::= entry identifier [ ( for identifier in discrete_range ) ] [ formal_part ] [ when condition ] is [ declarative_part ] begin sequence_of_statements [ handler_part ] end [ entry_simple_name ]; Within a task_body, up to one entry_body may be supplied for each entry declared in corresponding task_specification. The formal_part must conform to that in the task specification according to the rules in 6.3.1, and the discrete_range, if present, must match that supplied by the corresponding entry_declaration (else CONSTRAINT_ERROR, I suppose). A missing condition (guard) defaults to TRUE. If an entry_body has been supplied for an entry E, then the statement accept E; at the head of a select_alternative is equivalent to 16 when C => accept E D F do B end; where C, D, F, and B are the condition, discrete_range clause (if any), formal_part (if any), and sequence of statements and handler for E. However, names in C, D, F, and B are resolved according to the placement of the entry_body, rather than of the accept statement. Finally, if an entry_body has been supplied for E, then any accept statement for E must have this form. The evaluation of guards (see 9.7.1(5)) is liberalized slightly. For the execution of a selective wait, any conditions after when are evaluated in some order not defined by the language. Evaluation may stop when an open alternative with a waiting caller is found. (If enqueued-upon entries are chosen by priority, this liberalization will have to be partially deliberalized). When an accept statement appears alone, it is equivalent to a selective_wait with a single alternative. The syntax of select_statement is extended as follows. select_statement ::= selective_wait | select_all | conditional_entry_call | timed_entry_call select_all ::= select all; end select; For a task with entries E[0], E[1],..., a select_all is equivalent to the following selective_wait. select accept E0; or accept E ; 1 or ... or terminate; end select; [NB. The closing end select is unnecessarily verbose. I include it only for the benefit of pretty-printers.] 3.4 Example of layers I and II Figure 3 shows what happens to MODEL_TASK under the revisions proposed so far. 17 task type MODEL_TASK2 is pragma MONITOR; entry P0(X: DATA); entry E0(Y: DATA); entry E1(Z: DATA); end MODEL_TASK2; task body MODEL_TASK2 is S: STATE_DATA(N); -- N is some global variable. entry P0(X: DATA); begin UPDATE0(X, S); end; entry E0(Y: DATA) when TRUE is begin UPDATE1(Y, S); end; entry E1(Z: DATA) when OK(S) is begin UPDATE2(Z, S); end; begin loop select all; end select; end loop; end MODEL_TASK2; Figure 3: MODEL_ TASK redone with new syntax. 18 3.5 Layer III: Other features We could allow functions in task specifications. Each function would have to have a corresponding body in the task body. The semantics of selective_wait would be altered to account for function calls. Execution of calls on functions declared in a task_specification proceeds only when the task is waiting at a selective_wait. At that point, pending function calls may be executed concurrently; selection of one of the select_alternatives does not occur until no function call on the task is executing. It is erroneous for execution of such a function call to modify any of the objects declared in the task_body or the record part of the task_specification. Another possible addition is the requeue statement, which could be as proposed by the MRT. This could be added Ada 83 without change of syntax, by introducing a REQUEUE attribute on entry names. Thus, ``requeue AN_ENTRY'' becomes AN_ENTRY'REQUEUE. Of course, such a subprogram attribute has the odd property that it could only be allowed as the last statement (dynamically) in an accept body. 4 Conclusions I have prepared this document for several reasons. First, there was no evidence on paper that there had ever been a serious effort simply to ``fix'' Ada tasking, as opposed to adding a parallel and (in principle) redundant facility. We are told there was such an attempt, but absent any documentation, it is impossible to evaluate the conclusion. Lacking any serious alternatives, analysis of the MRT proposal is very difficult. Were Ada 9x a completely new language, this wouldn't matter as much; but our task is to get as much mileage as possible per unit change to an existing language. Second, several manufacturers have already invested time and effort in a ``passive'' task solution. Such a solution has obvious advantages for the transition to Ada 9x. One cannot object that ``these solutions are all different;'' that is only because of the lack of a standard solution, which is precisely the lack I am proposing to fix. There is a body of thought that says that processes and synchronizing data objects (mailboxes, monitors, protected records) are fundamentally different and must be distinguished. If this was the prevailing view in 1979, then we would be forced to conclude that the real-time community preferred Ada tasking even though they believed that it did not provide passive data objects, rejecting a language (RED) that made the distinction clear. I suspect, on the contrary, that programmers understand the strong relationships between communications with passive objects and communications with active 19 objects (the terminological confusion of ``message sending'' in object-oriented programming may be evidence of this). Their objection has been in the failure of promised performance to materialize and in the awkward (for the reader) organization of task bodies, which are addressed by the proposals above. But in any case, a programmer who believes in making a strong distinction is free to think of task FOO is ... and task FOO is pragma MONITOR; as two different constructs (a different set of keywords, after all). Others are free to recognize their semantic equivalence. The proposals presented here constitute a small-looking change from Ada 83 that overcomes the important performance difficulties---allowing compilers to produce efficient code for monitor tasks without recourse to prohibitively complex analysis. Indeed, since a non-real-time implementation is free to ignore the MONITOR pragma, the only required substantial change from Ada 83 for those implementations is the requeue attribute, the other proposed changes being simply syntactic sugar. Since the addition of the pragma (and its attendant restrictions) is perfectly valid in Ada 83, furthermore, there is no substantial reason to delay the introduction of the performance enhancements until 199x. I think that the mapping team is to be credited with, in effect, finding the necessary semantics for this pragma. Where we differ is that I see no reason to embody this semantics in a new construct. References [1] R. H. Campbell and A. Nico Habermann. The specification of process synchronization by path expressions. In E. Gelenbe and C. Kaiser, editors, Operating Systems, volume 16 of Lecture Notes in Computer Science, pages 89--102. Springer-Verlag, New York, 1974. [2] Charles Anthony Richard Hoare. Communicating sequential processes. Communications of the ACM, 21(8):666--677, August 1978. [3] INMOS Ltd. occam**(TM) Programming Manual. Prentice Hall International, 1984. 20