!topic LSN on Simplifications of Protected Objects in Ada 9X !key LSN-1047 on Simplifications of Protected Objects in Ada 9X !reference MS-9;4.6 !from Bob Duff $Date: 92/10/10 14:56:02 $ $Revision: 1.2 $ !discussion This Language Study Note discusses certain simplifications of protected objects made since MS;4.0. The terminology is changed. These things are no longer called protected records. Instead, we have protected types and protected objects. This change was made because to the client of a protected object, the object is not a record -- there are no (visible) components to access, regord_aggregates are not allowed, and so on. The syntax has been simplified. The reserved word 'record' is removed from the syntax. Now, a protected unit has a visible part (containing declarations of operations), and a private part (containing declarations of operations and components). The operations and components of the private part can be freely inter-mixed, making the syntax more uniform with other parts of the language, where we also allow free inter-mixing of declarative_items. Variant parts are no longer allowed in protected objects. Record representation clauses are no longer allowed for protected types. This is because protected types have too much implementation-defined stuff in them -- entry queues, bit masks, valid bits, and so on. The user can't count on the layout anyway; we don't think it makes much sense to allow the user to squeeze his own components in amongst the implementation-defined ones. Instead, to control the layout, one declares a protected type with one component that is of a record type, and applies a record representation clause to the record type. Cancellation of entry calls is now considered a protected action. This simplified the semantics of 'COUNT, because a cancelled call cannot be counted. That is, 'COUNT for a protected entry is a true count of the number of callers. Algorithms can rely upon 'COUNT in "last one out, close the door" examples, such as transient signals (see the Rationale), where the last task to leave resets some data to its normal state. This works even in the presence of abort statements. The rules for detection of exceptions in entry barriers have been simplified, thus simplifying efficient implementations. The old rule was that if a barrier raises an exception, then that same exception is propagated to the caller. The new rule is that if a barrier raises an exception, it is not considered to be an error in the task that happens to be at the head of that particular entry queue. Instead, it is considered to be a failure of the abstraction itself. Therefore, instead of raising a particular exception in a particular task, the semantics is that Program_Error is raised in all tasks waiting on all queues of the protected object. First, we explain why the new semantics is reasonable from the programmer's point of view. Then, we explain why it simplifies efficient implementations. With the old semantics, if a barrier gets an exception, then it will keep getting exceptions on every re-evaluation anyway (unless the user is evilly using side-effects); thus, that particular queue will be cleared out anyway. But if the state of the protected object is so damaged that a barrier evaluation raises an exception, then it is likely that other barriers will raise exceptions, too. Therefore, the only programs that will be hurt by the new semantics are the kind that appear in the ACVC! The new semantics is also reasonable because the caller can't be expected to repair any specific errors anyway. We have always intended that one possible implementation of protected types be to store a bit mask in each protected object indicating the current value of each barrier. This is likely to be very efficient, because the bits only need to be recalculated when the corresponding barrier values might have changed. We intend to clarify that this implementation is indeed correct; our latest documentation does not make it sufficiently clear. With the old exception semantics, the implementation was required to store an exception value for each barrier, in addition to the bit for that barrier. That probably means 33 bits per barrier (in most implementations) in addition to the queues themselves, plus extra run-time overhead keeping the data up to date. This is a heavy implementation burden for no user benefit. The new exception semantics allow the implementation to store one bit per barrier, plus a single "valid" bit. The valid bit indicates the validity of the barrier bits. Valid = True (the normal state) means that the barriers need not be reevaluated. When a barrier raises an exception, the implementation clears out all the queues, raising Program_Error in all those tasks, and sets Valid to False (all before unlocking the lock, of course) thus triggering barrier reevaluation the next time a task enters the protected object. One subtle point is that Valid is False initially, triggering barrier evaluation on the first use of the protected object. The more obvious implementation -- to evaluate the barriers and initialize the bit mask when the protected object declaration is elaborated -- is forbidden, because it causes order-of-elaboration problems.