Integrating Concurrency and Object-Orientation using Boolean, Access and Path Guards A. S. M. Sajeev and H. W. Schmidt Department of Software Development Monash University, Caulfield East, VIC 3145, Australia Email: fsajeev/
[email protected] Abstract Inheritance Anomaly is considered as a major problem in integrating object-orientation and concurrency. The anomaly forces redefinitions of inherited methods to maintain the integrity of concurrent objects. In this paper we discuss how the use of boolean, access and path guards attached to methods solve the problem of inheritance anomaly. Synchronization using boolean guards have known to be free of inheritance anomaly caused by partitioningof acceptable states. However, they cause anomalies from history sensitivity. We solve this using path guards. Path guards are similar to path expressions in the sense that both express the execution pattern of methods. However, while a path expression independently specifies the synchronization of a collection of methods, a path guard is attached to a method and it specifies the history of execution sequence(s) acceptable for executing the current method. Path expressions have been shown to cause inheritance anomaly, on the other hand, we will show that path guards form a solution to the problem.
1. Introduction In the design of concurrent object oriented programming (COOP) languages, inheritance has been found to be a major source of concern. Some COOP languages have, therefore, been designed without providing inheritance (Example: POOL [2]). However, the concept of inheritance is recognised by the OO community as one of the key features of object-orientation and therefore many researchers hesitate to accept languages without inheritance as object-oriented. Matsuoka and Yonezawa’s study of COOP languages [5] have shown that most of them exhibit the problem of ‘inheritance anomaly’. Inheritance anomaly occurs when “synchronization code cannot be effectively inherited without non-trivial class re-definitions”.
In this paper, we show how an extension of the 4P (Procedures with Preconditions as Parallel Programs) paradigm [6] can be used for concurrent object oriented programming without causing inheritance anomaly. (The 4P paradigm was originally designed to investigate transformation of sequential (not necessarily object-oriented) programs to parallel ones [7]).
2. The Extended 4P Paradigm A message can be passed to an object as a call or a send. The caller waits, while the sender spans a new thread to execute the corresponding method, while continuing its execution without waiting. In the case of "send" there can be no returned results.
2.1. Preconditions Each method has a precondition, also called a guard. There are three types of guards
Access guards (identified by the prefix ) Boolean guards (identified by the prefix ) Path guards (identified by the prefix ) The default guard is TRUE. An invoked method is executed if its guard holds. Otherwise it is delayed until the guard holds. Multiple invocations are executed concurrently. 2.1.1 Access guards An access guard specifies the mode of access to objects. There are three modes of access.
Observe (indicated by !) – In this mode other processes may also observe the attributes of the object however no process can change the attributes.
Weak exclusion (indicated by ) – Any access to the object is done as an atomic transaction.
Strong exclusion (indicated by ). – No other access to the object is allowed until either the current guard is evaluated to false or the current method execution is complete. 2.1.2 Path guards Path guards are expressed as regular expressions. They specify the history of accepted operations before the current operation becomes acceptable. The alphabet for the regular expressions are the set of method names in the class. We use the following symbols. r* r+ r? . [ˆr] pq p|q (r)
A sequence of zero or more executions of r One or more executions of r Zero or one executions of r Execution of any one method Execution of any method other than r p followed by q p or q Same as r
3. Bounded buffer example We will use the bounded buffer examples used by Matsuoka and Yonezawa to show how 4P handles inheritance anomaly. The followingBuffer class defines two methods put and get with their usual meanings. Class Buffer f const int SIZE = 100; int buf[SIZE]; int in, out; public: void put(int item); int get(); Buffer() in = 0; out = 0;
g
: in; : (in < out + SIZE) // guard void Buffer::put(int item) f buf[in++ % SIZE] = item; g : out; : (out < in) int Buffer::get() f return buf[out++ % SIZE]; g
As an example, a path:
:
p*q
as a guard to a method r means that an invocation of r is acceptable when the previous executions of methods in the object has followed the pattern: any number of executions of p followed by one execution of q. A path:
:
pq | qp
The methods put and get have synchronization guards attached to them. For instance, put can be accepted only when there is empty space in the buffer. This is indicated by the boolean guard (in < out+SIZE). However, the put method also needs exclusive access to the variable in so that two parallel calls to put will be serialised to avoid the possibility of one call overwriting the other call’s item in the buffer. Note that, unlike in languages like Ada, here calls to put and get can run in parallel thus increasing the level of parallelism without sacrifising modularity.
as a guard for r means that r is acceptable after executing p and q, the order of executing p and q being irrelevant.
4. Inheritance anomaly with guards
2.2. The role of super
Three causes for inheritance anomaly have been identified in [5]. They are:
The keyword super in a child’s method is used to indicate substitution of the guard or the body (depending on whether super appears in the guard or in the body) of the child’s method with that of the parent.
partitioning of acceptable states history-only sensitivity of acceptable states modification of acceptable states
4.1. Partitioning of acceptable states An object can have a set of states, which can be partitioned into disjoint subsets, where each subset represents a set in which certain methods are acceptable. (For instance, in the bounded buffer example, we can have three states: an empty state, a partially-full state and a full state.) When a new method is added in a subclass, this may cause further partitioning of the sets in the subclass. This can cause inheritance anomaly if the methods in the parent class’ synchronization or body needs to be changed to take account of the new partitions. Matsuoka and Yonezawa [5] have shown that this partition does not cause anomaly if the synchronization is based on guards because the guards can directly judge whether the message is acceptable or not in the current state.
4.2. History-only sensitivity of acceptable states With boolean guards to methods, synchronization based on history sensitivity can cause anomaly. This is because there are two different views in modelling the state of an object. In the external view, the state is captured by the external observable behaviour of the object. The internal view, on the other hand, is captured by the set of values of the object’s attributes. States based on the history of previous calls is one instance where the external view could be different from the internal view. Boolean and access guards alone cannot satisfy history sensitivity. For instance, suppose we want to create a child class of bounded buffer with a new method, gget which has the synchronization constraint that gget is acceptable only if the previous method accepted is not put. With boolean guards, this will cause anomaly since we will need to introduce a variable, say, afterPut which has to be set to TRUE in the put method and set to FALSE in the get and gget methods. Matsuoka [5]describes a solution to this problem. They localise the synchronization code within a separate synchronization specification of a class. Synchronization is specified using a combination of accept sets, transitions and guards. Transitions can change the accept set (ie set of acceptable methods). A transition corresponding to a method is executed immediately after the completion of the method body. They have defined different types of transitions like become, enable, disable, disable-once etc. Class Buffer f int size = in = out = 0; int item[BUFSIZE]; method_sets: mset EMPTY #fputg
mset FULL #fgetg mset PARTIAL EMPTY | FULL // both put and get ok in PARTIAL methods: void put(int item) f size--; out = (out+1) % BUFSIZE; ... g int get() f size++; in = (in+1) % BUFSIZE; ... g transitions: transition default f become EMPTY when (size == 0); become FULL when (size == BUFSIZE); become PARTIAL otherwise; g g // History sensitive buffer Class HistoryBuffer : Buffer f method_sets: mset AFTER-PUT #fggetg mset FULL super FULL | AFTER-PUT methods: int gget() f // return an item // only if the previous //call is not put return super get(); g transitions: transition put() f disable_once AFTER-PUT; g g Here put is specified to disable-once the acceptance of gget. However, it is easy to see that having keywords like disable-once is not a general solution; what if we want a ggget which can be accepted only if the previous three accepted calls are not put? Ferenczi [4] uses guarded methods interpreted as conditional critical regions to solve inheritance anomaly. His solution for the history sensitive buffer is: Class HistoryBuffer : Buffer f int gget() when (!after-put) f get(); g void put(int x) when (true) f super.put(x); after-put = True; g int get() when (true) f
int temp; temp = super.get(); after-put = False; return temp;
g
g
Even though Ferenczi has to define a new variable after-put to handle synchronization in the HistoryBuffer, it’s not considered as inheritance anomaly since the parent’s operations remain as such, and are referred to in the child with the qualifier super. However, some would consider this as a matter of interpretation. The 4P solution for a history sensitive buffer does not involve additional variable declarations or extensions to parent’s put and get methods: class HistoryBuffer: Buffer f public:
:
g
out; : (out < in); : (.*[ˆput]) | gget() f .... out++; g
The path guard specifies that gget is acceptable only if the sequence of methods previously accepted is either null (indicated by ) or any operations ended by one other than put. Regular expressions are powerful enough to express general sequences of methods, and are more general than Matsuoka’s method of keyword based transitions. It is also simpler than Ferenczi’s solution. However, Regular Expressions do have the limitation that they can denote only a fixed number of repetitions or an unspecified number of repetitions of a given construct. Two arbitrary numbers cannot be compared to check whether they are the same [1]. Whether we would need to express such synchronization constraints needs further investigation.
locked. Thus the acceptable states of put and get have been modified. The Lock class can be defined as: class Lock f public: Lock() flocked = 0;g
: locked void lock() flocked = 1;g
g
Let us also define a new buffer class with a function isEmpty which returns true if the buffer is empty. class ExtendedBuffer : Buffer f public: ExtendedBuffer() fin = 0; out = 0;g
g
int isEmpty() //acceptable always f return (in == out); g
Now we can mixin both Lock and ExtendedBuffer to create LockedBuffer. LockedBuffer can always accept isEmpty, but can accept put and get only when the buffer is not locked. class LockedBuffer: Lock,ExtendedBuffer f // when the buffer is locked // only isEmpty is acceptable. public:
:
super && locked; : super && (locked == 0) void deposit(int item);
:
super && locked; : super && (locked == 0) int withdraw();
4.3. Modification of acceptable states Modification of acceptable states occurs when the same method’s acceptable state in a child is different from its acceptable state in the parent. The example used is a mixin class where a class Lock (which defines two methods lock and unlock) is inherited by a LockedBuffer in which put and get are acceptable only when the buffer is not
: locked void unlock() flocked = 0;g
g
LockedBuffer() f locked = 0; in = 0; out = 0; g
Since the acceptable states of some of the operations have changed, we have to redefine the synchronization constraints, but only as an extension of the parent methods’ constraints.
5. Discussion The 4P paradigm integrates object-orientation and concurrency in an orthogonal manner by introducing a small number of constructs to specify synchronization in terms of access, boolean and path guards to methods. By separating synchronization from the method body, we are able to modify either of them independently. This is necessary as in the case of the LockedBuffer example discussed above. In paradigms where synchronization code is intertwined with the method body, such changes would be messy. We also solve the anomaly caused by history sensitivity (which languages based on guards generally don’t) by providing path guards. Path guards are similar to path expressions [3] in the sense that both express the execution pattern of methods. However, there are two main differences. Firstly, a path expression specifies the acceptable paths of execution of the methods. Whereas, a path guard specifies only the history of execution sequence(s) which will make the current method executable. Secondly, path guards are attached to methods while path expressions appear independently of methods. Even though path expressions separate synchronization from the method body, they have been shown to cause inheritance anomaly; a new method in the child would always force the path expressions to be rewritten to include the method in the paths.
References [1] A. V. Aho, R. Sethi, and J. D. Ullman. Compilers: Principles, Techniques and Tools. Addison Wesley, 1986. [2] P. America. POOL-T: A Parallel Object-Oriented Language, pages 199–220. MIT Press, Cambridge, Mass., 1987. [3] R. H. Campbell and R. B. Kolstad. Path expressions in pascal. In Proceedings of the Fourth International Conference on Software Engineering, pages 212–219, Munich, September 1979. [4] S. Ferenczi. Guarded methods vs. inheritance anomaly: Inheritance anomaly solved by nested guarded method calls. ACM SIGPLAN Notices, 30(2):49–58, February 1995. [5] S. Matsuoka and A. Yonezawa. Analysis of Inheritance Anomaly in Object-Oriented Concurrent Programming Languages, pages 107–150. MIT Press, Cambridge, Massachusetts, 1993. [6] A. S. M. Sajeev. Linguistic support for regulating processinstantiations. In D. Arnold, R. Christie, J. Day, and P. Roe, editors, Parallel Computing and Transputers, pages 51–57. IOS Press, Amsterdam, 1993. [7] A. S. M. Sajeev. Procedures with preconditions as parallel programs. Australian Computer Journal (Submitted), 1994.