!standard 9.5.1(2) 15-05-05 AI12-0129-1/08 !standard 9.5.1(4) !standard 9.5.1(5) !standard 9.5.1(7) !standard 9.5.3(15) !standard 9.5.3(23) !class binding interpretation 15-03-26 !status Corrigendum 1-2012 15-03-26 !status WG9 Approved 15-06-26 !status ARG Approved 9-0-2 15-03-26 !status work item 14-10-03 !status received 14-09-09 !priority Medium !difficulty Easy !subject Make protected objects more protecting !summary A Boolean aspect Exclusive_Functions is added to the language. !question The design of containers assumes that they are not accessed concurrently, on the ground that access can be protected with protected objects if necessary. However, containers provide functions that modify the state of the container; it is therefore not possible to access these through protected functions, as these can be called concurrently. Should there be a way to allow this? (Yes.) !recommendation Add a boolean aspect "Exclusive_Functions" for protected types and single protected objects to forbid concurrent execution of protected functions. !wording After 9.5.1(2) (i.e. at the end of the static semantics section) add: For a type declared by a protected_type_declaration or for the anonymous type of an object declared by a single_protected_declaration, the following language-defined type-related representation aspect may be specified: [Editor's note: The above is more complicated than the similar wording used for aspect Priority and the like, because we want to exclude derived protected types and protected/synchronized interfaces from the types that are included. Priority allows the former, and excludes the latter by a separate Legality Rule.] Exclusive_Functions - The type of aspect Exclusive_Functions is Boolean. If not specified (including by inheritance), the aspect is False. Redundant[A value of True for this aspect indicates that protected functions behave in the same way as protected procedures with respect to mutual exclusion and queue servicing (see below).] A protected procedure or entry is an *exclusive* protected operation. A protected function of a protected type *P* is an exclusive protected operation if the Exclusive_Functions aspect of *P* is True. Replace the first sentence of 9.5.1(4) by: A new protected action is not started on a protected object while another protected action on the same protected object is underway, unless both actions are the result of a call on a nonexclusive protected function. Replace 9.5.1(5) by: * Starting a protected action on a protected object corresponds to acquiring the execution resource associated with the protected object, either for exclusive read-write access if the protected action is for a call on an exclusive protected operation, or for concurrent read-only access otherwise; Replace 9.5.1(7) by: Redundant[After performing an exclusive protected operation on a protected object, but prior to completing the associated protected action, the entry queues (if any) of the protected object are serviced (see 9.5.3).] Replace 9.5.3(15) by: * If after performing, as part of a protected action on the associated protected object, an exclusive protected operation on the object, the entry is checked and found to be open. Replace 9.5.3(23) by: When the entry of a protected object is checked to see whether it is open, the implementation need not reevaluate the condition of the corresponding entry_barrier if no variable or attribute referenced by the condition (directly or indirectly) has been altered by the execution (or cancellation) of a call to an exclusive protected operation of the object since the condition was last evaluated. !discussion According to A(3/2), there is no correctness guarantee on concurrent calls involving overlapping objects passed by reference. Indeed, most implementations of the standard containers break when the same container is used concurrently, even when the container is declared constant. The usual way of allowing concurrent access to unsafe objects is to encapsulate them in a protected object. However, protected functions still allow concurrent access to (a constant view) of private objects. So the most natural way of writing a protected container is still unsafe: protected type Protected_Container is procedure Insert (Key : in Key_Type; Element : in Element_Type); function Element (Key : in Key_Type) return Element_Type; private Unsafe_Container : Ordered_Map_Instance.Map; end Protected_Container; Moreover, there is no easy way to actually realize this is unsafe, since it compiles without warning, and the problem may not show up in testing as the (current) implementation may not take advantage of the possibility of concurrent access, and even if it does, there is no guarantee that anything bad will happen (as it typical for race conditions). Programmers aware about the issue can restrict themselves to use only protected procedures and entries, which guarantees non-concurrency on private data: protected type Safe_Protected_Container is procedure Insert (Key : in Key_Type; Element : in Element_Type); procedure Element (Key : in Key_Type; Element : out Element_Type); private Unsafe_Container : Ordered_Map_Instance.Map; end Safe_Protected_Container; However, such a procedure Element is counter-intuitive, and makes client code heavier because contained elements can no longer be used in a more complex expression, explicit temporary variables have to be used instead. Also, this construction only works when Element_Type is definite. To use indefinite containers safely by using only protected procedures and entries, another layer is needed, e.g. using an indefinite holder or an access type. Moreover, the whole workaround is fragile, since there is no way to ensure protected functions won't be carelessly added in the future by a programmer not aware of the rationale. So the current version of Ada doesn't seem to adequately allow concurrency protection for objects whose read-only concurrent access is not safe, like standard containers, but it can be the case for user types as well. It is much easier and foolproof to have an aspect for protected types, that make protected functions have concurrency semantics of protected procedures, ensuring non-concurrent use of private objects, while remaining syntactically a function. From an implementation point of view, it would just mean that protected function calls would use the same semaphore as protected procedures; some implementations may even already use only one kind of semaphore for both. Implementation burden would likely range from null to minimal. --- Locking behavior might be implemented directly in the body of a protected type. As such, we don't allow specifying the Exclusive_Functions aspect for derived protected types, so that the compiler doesn't need to generate a new body with different locking. That seems like an excessive implementation burden, especially as the intended usage would never change the aspect. Similarly, we don't allow Exclusive_Functions on a protected interface. We don't want to have to come up with rules for inheritance and generic matching for this aspect (if there are multiple progenitors, they could have different values for the aspect); especially as the value of the aspect for an interface seems minimal (an interface has no bodies of its own). --- The aspect works on a protected type rather than on individual protected operations. This avoids problems caused by internal calls between protected functions with different exclusivity. For instance: protected type Ugh is function Exists (Key : in Key_Type) return True with Exclusive_Function; -- Not Ada!! function Element (Key : in Key_Type) return Element_Type; ... end Ugh; protected body Ugh is function Element (Key : in Key_Type) return Element_Type is begin if Exists (Key) then ... end if; end Element; ... end Ugh; In a case like this, the call to Exists would require exclusivity, but the outer call to Element would not. Thus, (depending on the implementation) there would be a need to acquire the exclusivity lock (or read-write lock) in the middle of a protected action, and a need to evaluate barriers at the end of the protected action even though it started non-exclusive. Grabbing the lock could of course suspend the operation within a protected action, something that is not supposed to happen. We can imagine adding additional rules to avoid problems like this, but that clearly is taking something simple and making it more complex. --- The design principle for protected objects is that barriers do not have to notice changes to global data that happen outside of the protected actions of the PO; but that barriers do notice changes to (any) data made by the protected actions. Thus, it is necessary for protected functions with the Exclusive_Functions aspect to evaluate barriers after each call. Such a function can safely update global data that is under control of the protected object. (It's not safe to do so from a regular protected function, as it might be executed concurrently.) An implementation can use the permission of 9.5.3(23) to avoid such re-evaluation of barriers if the barriers do not depend on any global data that might be modified by the function (this will often be the case). --- It has been suggested that if barriers need to be re-evaluated, then the protected object should have read-write access within the function. That seems to be too large a change for an aspect. It's always been the case that tying read-write to the the procedure syntax (and read-only to the function syntax) is unnecessarily limiting. All of the possible combinations make sense in some uses (that includes read-only access from a procedure, the least useful combination). Unfortunately, to fix that would require some sort of major syntactic change to protected types (perhaps explicitly specifying the protected object parameter so that the mode is exposed?) It is unlikely that we'll have enough interest in such a major change. Note that it has always been the case that a protected function can modify global data (this includes objects allocated from a storage pool but only accessible from the protected object). Exclusive_Functions makes this a safer thing to do (it probably won't work today on an implementation that uses a separate read-only lock, although [as with any race condition] it may take a long time to fail in practice). Thus, this is a useful feature in its own right; it's not exclusively tied to use for containers operations. Arguably, a more encompassing feature would be better, but it seems unlikely that a complex new feature would be worth the extra development time (both for the ARG and for implementers). The proposed feature is "good enough" to solve a variety of problems; it's unclear that a fancier feature would bring enough additional value to be worth the much higher costs. !example The example in the discussion could be written: protected type Protected_Container with Exclusive_Functions is procedure Insert (Key : in Key_Type; Element : in Element_Type); function Element (Key : in Key_Type) return Element_Type; private Unsafe_Container : Ordered_Map_Instance.Map; end Protected_Container; !corrigendum 9.5.1(2) @dinsa Within the body of a protected function (or a function declared immediately within a @fa), the current instance of the enclosing protected unit is defined to be a constant (that is, its subcomponents may be read but not updated). Within the body of a protected procedure (or a procedure declared immediately within a @fa), and within an @fa, the current instance is defined to be a variable (updating is permitted). @dinss For a type declared by a @fa or for the anonymous type of an object declared by a @fa, the following language-defined type-related representation aspect may be specified: @xhang<@xterm The type of aspect Exclusive_Functions is Boolean. If not specified (including by inheritance), the aspect is False.> @xindent A protected procedure or entry is an @i protected operation. A protected function of a protected type @i

is an exclusive protected operation if the Exclusive_Functions aspect of @i

is True. !corrigendum 9.5.1(4) @drepl A new protected action is not started on a protected object while another protected action on the same protected object is underway, unless both actions are the result of a call on a protected function. This rule is expressible in terms of the execution resource associated with the protected object: @dby A new protected action is not started on a protected object while another protected action on the same protected object is underway, unless both actions are the result of a call on a nonexclusive protected function. This rule is expressible in terms of the execution resource associated with the protected object: !corrigendum 9.5.1(5) @drepl @xbullet<@i a protected action on a protected object corresponds to @i the execution resource associated with the protected object, either either for concurrent read-only access if the protected action is for a call on a protected function, or for exclusive read-write access otherwise;> @dby @xbullet<@i a protected action on a protected object corresponds to @i the execution resource associated with the protected object, either for exclusive read-write access if the protected action is for a call on an exclusive protected operation, or for concurrent read-only access otherwise;> !corrigendum 9.5.1(7) @drepl After performing an operation on a protected object other than a call on a protected function, but prior to completing the associated protected action, the entry queues (if any) of the protected object are serviced (see 9.5.3). @dby After performing an exclusive protected operation on a protected object, but prior to completing the associated protected action, the entry queues (if any) of the protected object are serviced (see 9.5.3). !corrigendum 9.5.3(15) @drepl @xbullet @dby @xbullet !corrigendum 9.5.3(23) @drepl When the entry of a protected object is checked to see whether it is open, the implementation need not reevaluate the @fa of the corresponding @fa if no variable or attribute referenced by the @fa (directly or indirectly) has been altered by the execution (or cancellation) of a protected procedure or entry call on the object since the @fa was last evaluated. @dby When the entry of a protected object is checked to see whether it is open, the implementation need not reevaluate the @fa of the corresponding @fa if no variable or attribute referenced by the @fa (directly or indirectly) has been altered by the execution (or cancellation) of a call to an exclusive protected operation of the object since the @fa was last evaluated. !ASIS No impact. !ACATS test An ACATS C-Test should be created to check that the aspect is supported, and that no overlapping protected function calls are allowed when it is in effect. Even if the latter is wrong, it's likely that the test wouldn't detect it (detecting race conditions and the like can never be done reliably since they depend on timing), but we can at least try. !appendix From: Jean-Pierre Rosen Sent: Tuesday, September 9, 2014 10:56 AM This is my homework from the last meeting, although I subcontracted most of it to Natacha Porté who initially raised the issue (she's the one in the CC). [This is version /01 of the AI - Editor.] **************************************************************** From: Tucker Taft Sent: Tuesday, September 9, 2014 11:26 AM To me, this does not seem completely appropriate for an "aspect." This is a significant semantic change and should probably be reflected in a corresponding syntax change for protected functions. Now that we allow in-out parameters for function, perhaps something like the following would communicate the idea: protected type Protected_Container is ... function Element (in out Protected_Container; Key : in Key_Type) return Element_Type; ... **************************************************************** From: Steve Baird Sent: Tuesday, September 9, 2014 1:16 PM This particular proposal seems like a bad idea for several reasons: 1) Protected operation declarations do not normally mention the protected object as an explicit parameter; this proposal is therefore *very* different. 2) The "current instance" rule means that the name "Protected_Container" in the proposed declaration of Element denotes an object, not a type. (Obviously this objection could be worked around as was done with access_definitions). 3) Ambiguity. Is the explicit parameter instead of or in addition to the usual implicit parameter? Again, this could be worked around but it would be ugly. #1 is really the main objection. However, I agree with your objection to the original proposal. Perhaps this could be addressed by defining a new Convention; that might also help straighten out any interactions with interface types. **************************************************************** From: Jean-Pierre Rosen Sent: Tuesday, September 9, 2014 3:05 PM > To me, this does not seem completely appropriate for an "aspect." > This is a significant semantic change and should probably be reflected > in a corresponding syntax change for protected functions. Not such a big change, as it simply forces a semantic that is already allowed. Certainly not as big as synchronization => by_entry **************************************************************** From: Bob Duff Sent: Tuesday, September 9, 2014 4:20 PM I tend to agree with J-P here. What's the big deal? **************************************************************** From: Randy Brukardt Sent: Tuesday, September 9, 2014 4:37 PM Me too. This is just turning off an implementation permission. Sounds like an aspect to me. **************************************************************** From: Steve Baird Sent: Tuesday, September 9, 2014 4:47 PM I withdraw my objection to the aspect proposal. My objection would stand if we were talking about allowing functions to have a non-constant view of the protected object, but we aren't. **************************************************************** From: Tucker Taft Sent: Tuesday, September 9, 2014 9:10 PM I presumed we were wanting to be "honest" about the parameter mode for the protected object. I suppose if we are just keeping a constant view, then the aspect is adequate. **************************************************************** From: Jean-Pierre Rosen Sent: Sunday, March 1, 2015 9:20 AM Re: Reevaluating barriers after function calls [Editor's note: This topic was originally raised during the ARG phone meeting of February 26th, 2015. See the minutes of that meeting for details.] (my understanding is that it is only for mutually exclusive functions, otherwise it would be way too incompatible) My view of POs it that they are optimized for the case when the barriers only depend on the internal state. The behaviour is well defined when they depend on global states, but then "you'd better know what you are doing". For example, changing a global variable (from outside a protected action) will not unblock waiting tasks until a protected call to a subprogram/entry is executed. I have an example that does just that, and my PO has a null protected procedure which I call to force reevaluation of the barriers. That's why I'm not in favor of treating exclusive protected functions differently. I would hate having this AI rejected as a whole because of that feature. Or maybe make a different AI for the reevaluation of barriers? **************************************************************** From: Tucker Taft Sent: Sunday, March 1, 2015 12:04 PM Not sure what you are saying here. We are proposing that exclusive protected functions and protected procedures use the same wording as far as barriers. You indicate you are not in favor of treating exclusive functions "differently." Differently from what? Protected procedures, or non-exclusive functions? **************************************************************** From: Jean-Pierre Rosen Sent: Sunday, March 1, 2015 12:16 PM Differently from regular functions, i.e. never evaluate barriers for functions. Because the only things a function can change are outside the PO. **************************************************************** From: Tucker Taft Sent: Sunday, March 1, 2015 12:27 PM That is irrelevant. There is *no* permission to avoid re-evaluating barriers just because they reference a global variables, if the global variable is updated by a protected operation. The only concern is if the data referenced in a barrier is updated by something *other than* a protected operation. But using a global that is only updated by protected operations is perfectly fine. In fact, you indicate you are using a null protected procedure to ensure re-evaluation of the barriers. That actually does *not* work according to the rules of 9.5.3, if the update of the variables referenced in barriers occurred outside of any protected action. So I don't accept your argument... **************************************************************** From: Jean-Pierre Rosen Sent: Sunday, March 1, 2015 3:20 PM > That is irrelevant. There is *no* permission to avoid re-evaluating > barriers just because they reference a global variables, if the global > variable is updated by a protected operation. ? I never said that > The only concern is if > the data referenced in a barrier is updated by something *other than* > a protected operation. But using a global that is only updated by > protected operations is perfectly fine. Sure. > In fact, you indicate you are using a null protected procedure to > ensure re-evaluation of the barriers. That actually does *not* work > according to the rules of 9.5.3, if the update of the variables > referenced in barriers occurred outside of any protected action. In my case, the global was updated outside of a PO, and then the task that modified the variable called the null procedure to force reevaluation. Any reason why this would not work? My point is that there are two kinds of users: - Those who don't master the fine rules, and POs work perfectly as long as they don't access anything outside - Advanced users, and these can resort to forcing reevaluation. The initial proposal was simply to make POs usable to protect containers. Simple, grab the read/write lock instead of the read lock (provided they are different). Invisible to most users. Now we turn to changing the fine semantics of protected functions, for an assumed used case that nobody asked for. **************************************************************** From: Bob Duff Sent: Sunday, March 1, 2015 3:52 PM > In my case, the global was updated outside of a PO, and then the task > that modified the variable called the null procedure to force > reevaluation. Any reason why this would not work? Yes, that null procedure need not cause barrier evaluation: Implementation Permissions ... 23 When the entry of a protected object is checked to see whether it is open, the implementation need not reevaluate the condition of the corresponding entry_barrier if no variable or attribute referenced by the condition (directly or indirectly) has been altered by the execution (or cancellation) of a protected procedure or entry call on the object since the condition was last evaluated. 23.a Ramification: Changes to variables referenced by an entry barrier that result from actions outside of a protected procedure or entry call on the protected object need not be "noticed." For example, if a global variable is referenced by an entry barrier, it should not be altered (except as part of a protected action on the object) any time after the barrier is first evaluated. In other words, globals can be used to "parameterize" a protected object, but they cannot reliably be used to control it after the first use of the protected object. > My point is that there are two kinds of users: > - Those who don't master the fine rules, and POs work perfectly as > long as they don't access anything outside > - Advanced users, and these can resort to forcing reevaluation. As stated above, that need not work as one might expect. > The initial proposal was simply to make POs usable to protect > containers. Simple, grab the read/write lock instead of the read lock > (provided they are different). That's what I said during the phone meeting. But others argued (correctly, I think) that if we add this new feature, it will be used for other purposes, possibly involving barriers. It (now) seems clear to me that any protected operation that modifies variables must cause barrier evaluation for barriers that read those variables. So if we add a new kind of protected operation that can do that (like the new kind of protected function being discussed) then it must have that property. Or to look at it another way: It makes no sense to protect writes (with a read/write lock) but then ignore those writes as far as barriers are concerned. >...Invisible to most users. Now we turn to changing the fine semantics >of protected functions, for an assumed used case that nobody asked for. Well, the only alternative I can think of is to forbid what "nobody asked for" (i.e. forbid entries in these new cases). But that doesn't seem friendly. **************************************************************** From: Tucker Taft Sent: Sunday, March 1, 2015 3:54 PM >> That is irrelevant. There is *no* permission to avoid re-evaluating >> barriers just because they reference a global variables, if the >> global variable is updated by a protected operation. > ? I never said that But you said that an exclusive function could only update something "outside" of the PO. But that makes no difference, so long as nothing else is updating that same object. And note that the object updated might be reachable only via an access value that is "inside" the PO, so it can be sure that an update from outside won't happen. >> The only concern is if >> the data referenced in a barrier is updated by something *other than* >> a protected operation. But using a global that is only updated by >> protected operations is perfectly fine. > Sure. So being "inside" or "outside" is largely irrelevant. All that matters is whether the data is updated inside or outside a protected operation. >> In fact, you indicate you are using a null protected procedure to >> ensure re-evaluation of the barriers. That actually does *not* work >> according to the rules of 9.5.3, if the update of the variables >> referenced in barriers occurred outside of any protected action. > In my case, the global was updated outside of a PO, and then the task > that modified the variable called the null procedure to force > reevaluation. Any reason why this would not work? This does not work. If you read the permissions in 9.5.3, you will see that calling a null protected procedure does not ensure that all entry barriers are re-evaluated. See 9.5.3(23): "... the implementation need not reevaluate the condition of the corresponding entry_barrier if no variable or attribute referenced by the condition (directly or indirectly) has been altered by the execution (or cancellation) of a protected procedure or entry call on the object since the condition was last evaluated." So calling a null protected procedure doesn't meet the requirements for forcing re-evaluation. > My point is that there are two kinds of users: > - Those who don't master the fine rules, and POs work perfectly as > long as they don't access anything outside Accessing anything outside is not itself the problem. The problem is when code outside of a protected operation updates something that is referenced in an entry barrier. > - Advanced users, and these can resort to forcing reevaluation. Not really. There is no clear way to "force" reevaluation, according to 9.5.3, except by updating a variable inside a protected operation that is referenced by the entry barrier. > The initial proposal was simply to make POs usable to protect > containers. Simple, grab the read/write lock instead of the read lock > (provided they are different). Invisible to most users. Now we turn to > changing the fine semantics of protected functions, for an assumed > used case that nobody asked for. I don't agree. If you specify an exclusive function, we have to presume that you are updating a variable while in the function, and it seems perfectly possible that that variable is referenced in some entry barrier. If the function does *not* update any variable used in an entry barrier, then the implementation need not re-evaluate any barriers, according to 9.5.3(23). But without this proposed change, even if it *does* update such a variable, no re-evaluation need occur, which seems very error prone. **************************************************************** From: Jean-Pierre Rosen Sent: Monday, March 2, 2015 5:15 AM >> In my case, the global was updated outside of a PO, and then the task >> that modified the variable called the null procedure to force >> reevaluation. Any reason why this would not work? > > Yes, that null procedure need not cause barrier evaluation: [...] Yes, I missed that one. Too bad, maybe we need a PO'Reevaluate (procedure) attribute. [...] >> The initial proposal was simply to make POs usable to protect >> containers. Simple, grab the read/write lock instead of the read lock >> (provided they are different). > > That's what I said during the phone meeting. But others argued > (correctly, I think) that if we add this new feature, it will be used > for other purposes, possibly involving barriers. It (now) seems clear > to me that any protected operation that modifies variables must cause > barrier evaluation for barriers that read those variables. So if we > add a new kind of protected operation that can do that (like the new > kind of protected function being discussed) then it must have that property. > > Or to look at it another way: It makes no sense to protect writes > (with a read/write lock) but then ignore those writes as far as > barriers are concerned. OK, I understand the point. What worries me is that we are making a completely new kind of protected function, with really different rules. It is more than an aspect to my taste. Ah, if we had value returning procedures... **************************************************************** From: Steve Baird Sent: Monday, March 2, 2015 12:39 PM > Yes, that null procedure need not cause barrier evaluation: > > Implementation Permissions ... > 23 When the entry of a protected object is checked to see whether it > is open, the implementation need not reevaluate the condition of the > corresponding entry_barrier if no variable or attribute referenced by > the condition (directly or indirectly) has been altered by the > execution (or cancellation) of a protected procedure or entry call on > the object since the condition was last evaluated. I've updated the proposed wording for AI12-0129 to reflect the discussion of the last phone meeting - see attached. This includes updating the permission Bob cited to treat a protected function with the new aspect set like a protected procedure. [This updated wording is in version /04 of the AI. The discussion was updated by yours truly based on this e-mail thread - Editor.] **************************************************************** From: Tucker Taft Sent: Monday, March 2, 2015 1:14 PM Overall looks good, but it seems odd for the above definition to be buried in the definition of the Exclusive_Functions aspect description. Couldn't we move it to shortly before the place it is first used? **************************************************************** From: Tucker Taft Sent: Monday, March 2, 2015 1:16 PM Also, it might be easier to understand if it were phrased positively: A protected operation is said to be *exclusive* if it is a protected procedure, a protected entry, or a protected function with the Exclusive_Functions aspect True. **************************************************************** From: Steve Baird Sent: Monday, March 2, 2015 1:36 PM > ... it seems odd for the above definition to be buried in the > definition of the Exclusive_Functions aspect description. > Couldn't we move it to shortly before the place it is first used? I disagree, but I don't feel strongly. > Also, it might be easier to understand if it were phrased positively: > > A protected operation is said to be *exclusive* if it is a protected > procedure, a protected entry, or a protected function with the > Exclusive_Functions aspect True. I disagree strongly, but I could downgrade my objection to weak if you reorder things. An animal is said to be *scary* if it is a lion, a tiger, or a bear having sharp teeth. Is a lion without sharp teeth scary? Reordering your proposed wording resolves this issue, although I still think the list-less (or list of length one, depending on how you view it) version is simpler to understand. Compare A nonnegative integer is said to be *positive* if it is not zero. with A nonnegative integer is said to be *positive* if it is 1, or 2, or 3, or ... but I'll concede that a list of length three is not the same as an infinite one. **************************************************************** From: Tucker Taft Sent: Monday, March 2, 2015 1:47 PM A bit of a strawman! I would say expressing things "positively" would have been to say: A nonnegative integer is said to be *positive* if it is greater than zero. In any case, I'll sort of accept your point about the possible ambiguity, though since the attribute is named "Exclusive_Functions" I think the ambiguity is actually pretty unlikely. It also feels better to me to start with the two "unconditional" members of the "exclusive" club, so re-ordering would not clarify things overall in my view. Let's see how others weigh in. No biggies either way. **************************************************************** From: Randy Brukardt Sent: Thursday, March 5, 2015 10:36 PM ... > I've updated the proposed wording for AI12-0129 to reflect > the discussion of the last phone meeting - see attached. As usual, Steve left the updates to the discussion to your poor editor, who in this case needed to justify something that he didn't understand. Luckily, Tucker had to explain it to Jean-Pierre, so I was able to steal some of that. Hope I got it right: The design principle for protected objects is that barriers do not have to notice changes to global data that happen outside of the protected actions of the PO; but that barriers do notice changes to (any) data made by the protected actions. Thus, it is necessary for protected functions with the Exclusive_Functions aspect to evaluate barriers after each call. Such a function can safely update global data that is under control of the protected object. (It's not safe to do so from a regular protected function, as it might be executed concurrently.) An implementation can use the permission of 9.5.3(23) to avoid such re-evaluation of barriers if the barriers do not depend on any global data that might be modified by the function (this will often be the case). >> ... it seems odd for the above definition to be buried in the >> definition of the Exclusive_Functions aspect description. >> Couldn't we move it to shortly before the place it is first used? > >I disagree, but I don't feel strongly. I disagree with both of you. :-) I agree with Tucker that it's odd to put the definition in the middle of the aspect, but I don't agree with his choice of place to put it. Those uses are in Dynamic Semantics, but this is a statically determined property, so the definition makes more sense in the Static Semantics section. Thus, I'd suggest something like: After 9.5.1(2) (i.e. at the end of the static semantics section) add: For a type declared by a protected_type_declaration or for the anonymous type of an object declared by a single_protected_declaration, the following language-defined type-related representation aspect may be specified: Exclusive_Functions - The type of aspect Exclusive_Functions is Boolean. If not specified (including by inheritance), the aspect is False. [A value of True for this aspect indicates that protected functions behave in the same way as protected procedures with respect to mutual exclusion and queue servicing (see below).] A protected operation is said to be *exclusive* unless it is a protected function and the Exclusive_Functions aspect of the associated protected type is False. The only change here is the placement of the paragraph defining "exclusive" and its indentation. >> Also, it might be easier to understand if it were phrased positively: >> >> A protected operation is said to be *exclusive* if it is a protected >> procedure, a protected entry, or a protected function with the >> Exclusive_Functions aspect True. > >I disagree strongly, but I could downgrade my objection to weak if you reorder >things. I agree with Steve (disagree with Tucker). Not mentioned by Steve is the fact that Tucker oversimplified the description of the Exclusive_Functions aspect here, as it is an aspect of a protected type, not of a protected function. It has to be wording more like Steve's wording: A protected operation is said to be *exclusive* if it is a protected procedure, a protected entry, or a protected function and the Exclusive_Functions aspect of the associated protected type is True. Now, Steve's complaint is a lot more realistic (although the commas help). Moreover, Tucker's version has a lot more words, and all other things being equal, it's harder to understand by sheer heft. The "unless" potentially balances that a bit, but the combination of a bunch of "or"s with an "and" make that a wash in my view. Besides, I find both versions plenty clear; "unless" is only a problem when the terms on either side are complex; that's not the case here. So I don't understand the objection. **************************************************************** From: Bob Duff Sent: Friday, March 6, 2015 8:09 AM > As usual, Steve left the updates to the discussion to your poor > editor, who in this case needed to justify something that he didn't > understand. Luckily, Tucker had to explain it to Jean-Pierre, so I was > able to steal some of that. Hope I got it right: Yes, it looks right. You could refer to whatever paragraphs in the AARM that explain this principle. > The design principle for protected objects is that barriers do not have to > notice changes to global data that happen outside of the protected actions > of the PO; but that barriers do notice changes to (any) data made by the > protected actions. Thus, it is necessary for protected functions with the > Exclusive_Functions aspect to evaluate barriers after each call. > Such a function can safely update global data that > is under control of the protected object. (It's not safe to do so from a > regular protected function, as it might be executed concurrently.) > > An implementation can use the permission of 9.5.3(23) to avoid such > re-evaluation of barriers if the barriers do not depend on any global data > that might be modified by the function (this will often be the case). ... > A protected operation is said to be *exclusive* if it is a protected > procedure, a protected entry, or a protected function and the > Exclusive_Functions > aspect of the associated protected type is True. I agree with Steve's complaint about the ordering, and agree with Tucker that it's better to put the "main" cases (procs&entries) first. So: A protected procedure or entry is an "exclusive" protected operation. A protected function is exclusive if the Exclusive_Functions aspect of the protected type is True. I don't like "said to be" and similar. We avoided such wording in Ada 9X, and I hope it hasn't crept back in. We make the rules, so we can just say "is". "...said to be" makes it seem like we're a news reporter quoting anonymous sources or rumors. "the protected type" is shorter than "the associated protected type", and equally clear. **************************************************************** From: Tucker Taft Sent: Friday, March 6, 2015 8:22 AM > ... I agree with Steve's complaint about the ordering, and agree with > Tucker that it's better to put the "main" cases (procs&entries) first. So: > > A protected procedure or entry is an "exclusive" protected > operation. A protected function is exclusive if the > Exclusive_Functions aspect of the protected type is True. The above looks good to me. **************************************************************** From: Tullio Vardanega Sent: Friday, March 6, 2015 8:40 AM So it does to me too. **************************************************************** From: Bob Duff Sent: Friday, March 6, 2015 8:30 AM I think the !discussion of AI12-0129 should explain why the current instance is not 'in out' for these new kind of functions. I also think this AI should not be approved until there's an implementation of some container using it -- I'd choose Vectors. Vectors has a function with an 'in out' Vector parameter, and I'm not sure how to deal with that. I don't know if AdaCore would be willing to have someone (like me) do this work. Implementation of the aspect looks easy. I'm more concerned about usability -- if we have a good protected Vector, then I'll be convinced that the aspect works well. **************************************************************** From: Jean-Pierre Rosen Sent: Friday, March 6, 2015 8:48 AM > I also think this AI should not be approved until there's an > implementation of some container using it Well, it's not intended to make new containers, but to protect existing ones. OK, you may view it as a new container. > I don't know if AdaCore would be willing to have someone (like me) do > this work. Implementation of the aspect looks easy. I'm more > concerned about usability -- if we have a good protected Vector, then > I'll be convinced that the aspect works well. I'll see with Natacha (who triggered this whole stuff) if she can provide her initial "protection" of containers. **************************************************************** From: Bob Duff Sent: Friday, March 6, 2015 9:09 AM > Well, it's not intended to make new containers, but to protect > existing ones. OK, you may view it as a new container. Not sure what you mean. We're trying to allow someone to write a generic package that exports a protected Vector type, right? > I'll see with Natacha (who triggered this whole stuff) if she can > provide her initial "protection" of containers. Great! What would it look like? A generic package Protected_Vectors that exports a protected type Vector. It instantiates Ada.Containers.Vectors, and Protected_Vectors.Vector has a component of type Ada.Containers.Vectors.Vector. And the protected type exports wrappers for all the operations. So clients say "My_Vector.Append(...);" Or like this: Hide that protected type, and export wrappers of the wrappers. Try to make the interface identical to Ada.Containers.Vectors. So clients say "Append(My_Vector, ...);" (or can optionally use prefix notation, assuming it's tagged). Or something else? I want to make sure that all the "implicit magic" works nicely (indexing and "for ... of" notation and so on). I found the containers to be nearly unusable until that syntactic sugar was added. It was just an extreme case of "safety" over "readability". **************************************************************** From: Randy Brukardt Sent: Friday, March 6, 2015 1:54 PM > I think the !discussion of AI12-0129 should explain why the current > instance is not 'in out' for these new kind of functions. The only reason I have is that it is too big a change for a mere aspect. That seems obvious, so what's to explain??? > I also think this AI should not be approved until there's an > implementation of some container using it -- I'd choose Vectors. > Vectors has a function with an 'in out' Vector parameter, and I'm not > sure how to deal with that. Why does this matter? It doesn't make sense for a complete container to be a protected object. There are many operations (all of the callback ones for starters) which would deadlock if used as intended. Plus there would be so many operations that no one could ever tell if the implementation is correct. (Protected types need to be small.) If you want a task-safe container type, one has to design one with built-in locks (most likely locks that allow the same task to grab it again, to allow the call-back cases). The purpose of this feature, as I see it, is to provide a proper wrapper for an application-specific container use. Such a use might contain functions. (IMHO, functions in protected object should *always* have worked this way, since POs are solely about exclusion, but too late for that.) But you'd want the PO to use names and operations that make sense for the application; general container operations are unlikely to do that. It's not to provide, in any way, general task-safe container. I've been thinking about that, and I think the solution there lies with moving most of the checking (including tampering) to compile-time checks. That's going to require some sort of new feature and definitely requires a different specifications for some of the container operations. And that's a multi-year project. There's already an empty AI for that project (AI12-0139-1, which I see I have mis-classified as "easy"!), feel free to make suggestions for it. This AI is about a quick-fix (really about guaranteeing a property which is true in practice on most implementations anyway, and should always have been an option on all implementations) that can be used to write custom application-specific protected types. Including those that happen to wrap containers. If we keep expanding the scope, then there is no way it can go into the Corrigendum, at which point spending any time on it at all is pointless (because we ought to have some sort of task-safe containers to go with the parallel stuff in Ada 202x). **************************************************************** From: Bob Duff Sent: Friday, March 6, 2015 4:07 PM > > I think the !discussion of AI12-0129 should explain why the current > > instance is not 'in out' for these new kind of functions. > > The only reason I have is that it is too big a change for a mere aspect. > That seems obvious, so what's to explain??? It's not obvious to me. It's a matter of good taste, and many/most of us agree with your "too big" statement. But anyway, I think you've got it backwards. The reasoning in the emails seems to be: 1. We don't need the current instance to be 'in out'. 2. Therefore this feature can be an aspect in good taste. I think the AI (and probably the AARM) should say that (1) is true, and explain WHY, and then state (2) as our opinion. At this point I don't really understand why (1) is true, given that existing containers have functions with 'in out' parameters. If (1) is false, and your good-taste argument is correct, then we've got the wrong design! That is, we shouldn't say: 2. Let's use an aspect. 1. Therefore we can't have 'in out', ignoring the possibility that we need it. That's what I'm saying is backwards. We decided that if protected functions lock in the same way that protected procedures do, then obviously they should trigger entry servicing like procedures do. But then it also seems obvious (at least at first glance) that these new functions should behave like protected procedure as far as 'in out' goes, too. If that's not true, we ought to explain why. More later... **************************************************************** From: Randy Brukardt Sent: Friday, March 6, 2015 5:03 PM ... > But anyway, I think you've got it backwards. The reasoning in the > emails seems to be: > > 1. We don't need the current instance to be 'in out'. > 2. Therefore this feature can be an aspect in good taste. > > I think the AI (and probably the AARM) should say that (1) is true, > and explain WHY, and then state (2) as our opinion. (1) is not true. (1) has never been true, but the Ada 9x team ignored multiple complaints about that and did not provide any mechanism for controlling the mode of the instance (or the locking mode for that matter). It's way too late to change that design decision, unless someone wants to make a substantial redesign of protected types. That's not me. :-) > At this point I don't really understand why (1) is true, given that > existing containers have functions with 'in out' > parameters. The existing containers *only* have writing routines with 'in out' parameters. It doesn't make sense to write a container that's part of a protected object from within a function, because of the Ada 9x team's design decisions that everything is a constant within a function. Ergo, we don't need any writing functions and thus the aspect is sufficient for anything that one could do. [Of course, you can write a *global* from inside of a protected function, and that works fine, so there again doesn't need to be any variable access to the PO. It's the variable global object case that causes us to worry about barriers; but that's simply saying that barriers can be re-evaluated; the ImplPerm says that it's not necessary unless the barriers contain something changed by the function. In most cases, the effect would be the same either way. I suspect the language would have made more sense if we had said that barriers are *always* re-evaluated, and then lean on the ImplPerm to eliminate the overhead, but again, it's way too late for that.] If you're worrying about wrapping Reference in a protected type, you can do it by making the container "access-to" in the protected object. But quite honestly, I don't care that much. I think this is a stop-gap for writing application-specific protected types, in which case it's unlikely that one will need a generalized way to write arbitrary parts (often, the element is a private type anyway, so there are no parts to write). And it's also fixing (in part) a dubious design decision from way back in Ada 9x. I talked about why I don't think this has any bearing on the long-term solution to the problem, but we're not going to make a long-term fix in this Corrigendum. We should at least provide a short-term fix in this Corrigendum, because otherwise there will be no portable way to do it. ... > We decided that if protected functions lock in the same way that > protected procedures do, then obviously they should trigger entry > servicing like procedures do. But then it also seems obvious (at > least at first glance) that these new functions should behave like > protected procedure as far as 'in out' goes, too. If that's not true, > we ought to explain why. I have to admit, when you put it this way, it makes me realize just how screwed up the description of protected types is. It makes no sense to me to talk about a "read-write lock"; it's just an "exclusion lock", because the only thing anyone ever wants from one of these guys is exclusion. The two lock model makes no sense in practice, especially on multiprocessors. I think the idea was that on monoprocessors the locks could be dispensed with altogether, but given that hardly anything is a monoprocessor these days, that's hardly relevant anymore. Anyway, this is clearly a case where "best" is the enemy of good enough. Protected types should always have had a way to specify what sort of access is given to the protected components. All of the possibilities make sense (some more than others): function-constant, function-variable, procedure-variable, procedure-constant (if one needs to read multiple things at once), entry-variable, and probably even entry-constant. To do that, we'd need to make the protected object an explicit parameter to the protected operations, so we can give it a mode. But mixing that and "traditional" protected types would be mystifying. So we'd have in effect an entire new kind of protected type. I can't imagine a circumstance where that would happen. Some people tried to make the case back during Ada 9x that we needed that, and were rebuffed. I doubt that enough has changed in the interim to give us the will to make such a major change. So when you say: > 2. Let's use an aspect. Because it's the only thing that we can do now (meaning in the next 5 years), and most likely the only thing we'll have will to do in the foreseeable future. The only alternative is a complete redesign of either protected object or of the containers, and neither are going to be fast or likely. > 1. Therefore we can't have 'in out', ignoring the possibility that > we need it. Whether we need it or not is irrelevant (and I don't think we really need it), because there is nothing else that we can do in the near future that will even help the need. This proposed feature is plenty good to meet most of the needs, and if it can't meet 100% of the needs (and what feature can??), it's still good enough to me. (Especially as most implementations already work more or less this way.) It a bonus that it brings protected types closer to the way I think they should have worked in the first place. (Like Detect_Blocking, I think this should have been the default, but sadly, it's too late for that.) **************************************************************** From: Bob Duff Sent: Friday, March 6, 2015 5:06 PM > Why does this matter? I'm trying to protect against adding yet another useless feature. Or a useful feature that has annoying restrictions. >...It doesn't make sense for a complete container to be a protected >object. There are many operations (all of the callback ones for > starters) which would deadlock if used as intended. Plus there would >be so many operations that no one could ever tell if the >implementation is correct. (Protected types need to be small.) If that's all true (which I don't concede) then we still should have a realistic example using the proposed feature, using it as intended (and I must admit I don't fully understand what's intended). **************************************************************** From: Randy Brukardt Sent: Friday, March 6, 2015 5:26 PM > > Why does this matter? > > I'm trying to protect against adding yet another useless feature. Fair enough. But that's not possible in this case, this is how protected types ought to work in my view. As I noted in my last message, I think this should be the default, and the other way should be forgotten. Just like Detect_Blocking. > Or a useful feature that has annoying restrictions. That's caused by the stupid Ada 9x people (sorry), who were repeatedly told that there is no connection between functions and constancy, but decided to ignore it anyway. (Part of that probably was because they also decided to ignore the need for "in out" parameters on functions, but two wrongs don't make a right! :-) We're stuck with that now, there is no conceivable way to change that. There is no realistic possibility that we'd get consensus go for new syntax for protected types - the problem would have to be 10 times worse. I don't see that the barrier question matters at all within the intended use. The argument is that it would be inconsistent with the rules to not reevaluate them, which makes sense, but I don't really care either way. > >...It doesn't make sense for a complete container to be a protected > >object. There are many operations (all of the callback ones for > > starters) which would deadlock if used as intended. Plus > there would > >be so many operations that no one could ever tell if the > >implementation is correct. (Protected types need to be small.) > > If that's all true (which I don't concede) then we still should have a > realistic example using the proposed feature, using it as intended > (and I must admit I don't fully understand what's intended). My understanding is the use is for simple wrappers of functions like Element. The example in the AI is (with the new aspect in place): protected type Protected_Container with Exclusive_Function is procedure Insert (Key : in Key_Type; Element : in Element_Type); function Element (Key : in Key_Type) return Element_Type; private Unsafe_Container : Ordered_Map_Instance.Map; end Protected_Container; I've written a number of protected types like this for my mail and web server stuff, and it might have made sense to use a container for the implementation rather than writing my own queue routines as linked lists (for one example). But one needs something like Exclusive_Functions in order to make access to the container 100% safe. (And that's true of pretty much anything that you'd write yourself, thus I think Exclusive_Functions should have been the default.) I wouldn't expect the entire set of container operations to be wrapped, just the handful that the application actually is going to use. Thus, it's unlikely that the full generality of the containers packages would be used. **************************************************************** From: Bob Duff Sent: Friday, March 6, 2015 6:05 PM > Anyway, this is clearly a case where "best" is the enemy of good > enough. OK, I guess I can buy that. :-( **************************************************************** From: Tucker Taft Sent: Friday, March 6, 2015 6:36 PM Historically, I think it is important to remember that Ada 83's rendezvous-based synchronization was supposed to be sufficiently flexible to solve all of the programmer's problems, so long as compilers implemented "easy" optimizations such as passive tasks, etc. But that was unrealistic in various ways, in part because the semantics of rendezvous had so many subtleties that couldn't be trivially "passivated." So we tried to keep protected types relatively simple, working basically only one way, where functions were read-only, and procedures were read-write, and entries were blocking. In retrospect you could argue that we should have made them more general and flexible, but I think their relative simplicity and single-mindedness and "close to the metal" feel was an important contrast with rendezvous, and has been an important part of their rapid adoption by Ada programmers, including those doing hard real-time systems. I realize this is a bit off topic, but I think we should keep some historical perspective here. **************************************************************** From: Randy Brukardt Sent: Friday, March 6, 2015 9:21 PM Sure, the Ada 9x team had reasons for the choices that they made. You didn't even mention the heavy pressure to simplify, which I think had a lot of effect on these particular decisions, as there wasn't an obvious way to provide the extra capabilities without significant changes. Anyway, right or wrong or (more likely) in between somewhere, protected types aren't going anywhere, and I doubt we're going to be making major changes to them anytime soon. Thus this aspect is about as much as we can justify doing here; it appears to be relatively cheap to implement, and it appears to at least fix part of the problem. Hard to ask for more than that. **************************************************************** From: Bob Duff Sent: Saturday, March 7, 2015 8:40 AM > Historically, I think it is important to remember that Ada 83's > rendezvous-based synchronization was supposed to be sufficiently > flexible to solve all of the programmer's problems, so long as > compilers implemented "easy" optimizations such as passive tasks, etc. > But that was unrealistic in various ways, in part because the semantics of > rendezvous had so many subtleties that couldn't be trivially "passivated." Right, and it's not just a matter of efficiency. Sometimes those features are too high level and just get in the way. > So we tried to keep protected types relatively simple, working > basically only one way, where functions were read-only, and procedures were read-write, and entries were blocking. Simple is good. And the 3-way distinction (RO, RW, blocking) is good. The unfortunate choice was to use function/procedure keywords to mean RO/RW. That choice was quite natural, given the restriction on 'in out' params on functions. So that Ada 83 restriction is the root of our troubles here (combined with the fact that you inexplicably liked that restriction, so were willing to build upon it). > In retrospect you could argue that we should have made them more > general and flexible, but I think their relative simplicity and > single-mindedness and "close to the metal" feel was an important > contrast with rendezvous, and has been an important part of their rapid adoption by Ada programmers, including those doing hard real-time systems. This AI isn't trying to make them (semantically) more general and flexible, nor ruin the relative simplicity and so forth. The same 3-way distinction remains. This AI is purely about syntactic sugar -- for RW operations, can I use function-call notation, or do I have to return the result via a procedure's 'out' param. If you had chosen a different syntax to make the 3-way distinction, you could still honestly say all of the above stuff about "simplicity" and "close to the metal" and so forth. And we wouldn't have this AI. > I realize this is a bit off topic, ... Not really. >...but I think we should keep some > historical perspective here. Agreed. **************************************************************** From: Tucker Taft Sent: Saturday, March 7, 2015 9:09 AM I think I was influenced by the SPARK folks here. But I now agree that so long as you don't introduce evaluation order dependence by doing so, having in-out parameters in functions is appropriate. But that evaluation-order dependence thing is a bit of a can of worms, and would not have been do-able as part of the Ada 9X process. ****************************************************************