Object Synchronizer: A Design Pattern for Object Synchronization Ant´onio Rito Silva1 , Jo˜ao Pereira2 and Jos´e Alves Marques1 INESC/IST Technical University of Lisbon, Rua Alves Redol no 9, 1000 Lisboa, PORTUGAL 2 INRIA, Domaine de Voluceau, Rocquencourt, B.P. 105, 78153 Le Chesnay Cedex, FRANCE
[email protected],
[email protected],
[email protected]
1
This pattern was workshopped at the European Conference of Pattern Languages of Programs (EuroPLoP96), Kloster Irsee, Germany, July, 1996.
writable. A shape is private when it is only visible to the user that has created it: its owner. A shape is publicly readable when it is visible to all users but only its owner can update it. A shape is publicly writable when any user can see and modify it. A possible architecture for such an application contains several client applications having their own objects (application space) and sharing a set of domain objects (domain space) which may be kept in a data store (persistent space). The application space has interface objects, e.g scrollbars and shapes; the domain space has shared objects, e.g. shapes data; and the persistent space stores documents, e.g. a graphical document including its shapes’ data.
Abstract This paper describes the Object Synchronizer pattern which decouples object synchronization from object functionality. This pattern supports several synchronization policies and their customization. This pattern is used when invocations to an object need to be controlled in order to preserve its consistency. The solution described by this pattern provides encapsulation, modularity, extensibility and reuse of synchronization policies.
3.2. Problem
1. Intent
Client applications execute operations that invoke methods on shape objects. Invocations on a non-private shape object must be controlled either because it is a publicly readable shape where update invocations from non-owners should return an error; or because it is a publicly writable shape where simultaneous invocations by different client applications are liable to result in corruption of the shared shape’s state. Traditional solutions for object synchronization use:
The Object Synchronizer pattern abstracts several object synchronization policies. It decouples object synchronization from object functionality.
2. Also Known As Object Concurrency Control. Object Serialization.
The persistent space’s locking mechanisms to synchronize invocations to shared resources. However, a shared object may just be volatile and the effort to make it persistent may result in unacceptable overhead. Another issue is that the supported locking mechanisms may not be suitable for the synchronization needs of the application.
3. Motivation 3.1. Example Consider the design of a Cooperative Drawing Application allowing cooperative manipulation of graphical documents. Users at different terminals can simultaneously access the same graphical document and changes made to a local view are immediately propagated to other local views. Due to the cooperative characteristics of the application a shape can be made private, publicly readable or publicly
Synchronization mechanisms, as semaphores [1] and monitors [2], to synchronize accesses. However, this results in code tangling which forbids, both functionality and synchronization independent reuse. For instance, a shape’s functionality code should be the 1
same for private, publicly readable or publicly writable shapes.
proceed or can delay or reject them. The Shape Synchronization Interface synchronizes invocations delegating on Synchronization Policy. Operations preControl and postControl of the Synchronization Policy control the order of invocations according to a given synchronization policy.
3.3. Forces An object-oriented solution for the object synchronization problem must resolve the following forces:
Extensibility is achieved by providing different implementations of the Synchronization Policy. Each implementation provides a specific synchronization semantics. Actually, due to synchronization complexity, Synchronization Policy class is a Fac¸ade [3] encapsulating several synchronization objects.
Extensibility requires abstraction of synchronization policies. It is not possible to find an optimal policy for all situations: policies should be customizable. The most suitable policy depends on the domain object and its operations semantics. For instance, some domain objects are frequently accessed, thus requiring a pessimistic policy, whereas others are not, making the use of an optimistic policy more efficient.
Modularity is achieved through the implementation of synchronization in Synchronization Policy class and so it is decoupled from Shape.
Modularity requires separation of object synchronization from object functionality. This orthogonality allows synchronization policy switching with no repercussions on other components, as well as incremental introduction of synchronization.
Encapsulation is achieved because the Shape Synchronization Interface isolates synchronization from the Client Object.
Encapsulation requires the synchronization part of an object to be placed within the object itself rather than spread out among its clients. This places synchronization responsibility within the object, thereby avoiding client negligence.
Reusability is achieved because a Shape objects can be associated with different Synchronization Policy objects. This allows independent reuse of Shape and Synchronization Policy classes.
Reusability requires separate reuse of functionality and synchronization code. It should be possible to independently reuse both synchronization code and functionality code. For instance, a shape functionality code is reused for private, publicly readable and publicly writable shape classes.
4. Applicability Use the Object Synchronizer pattern when:
Invocations to an object must be controlled to preserve its consistency - invocations must be controlled according to a synchronization policy.
3.4. Solution
It is premature to decide on which object synchronization policy to use. - At initial stages of the development process it may be too early to choose a specific policy. Moreover it should be possible to test several policies before choosing.
Figure 1 sketches a solution for the synchronization of a Shape object. Client Object
Shape Synchronization Interface moveBy()
Synchronization Policy
Shape
Different synchronization policies may be used by different objects of a class - different synchronization and sharing semantics1 are often required by applications, e.g. groupware systems.
moveBy()
synchronization->preControl() shape->moveBy() synchronization->postControl()
5. Structure and Participants
preControl() postControl()
The UML [4] class diagram in Figure 2 illustrates the structure of the Object Synchronizer pattern. The main participants in the Object Synchronizer pattern are:
Figure 1. Synchronized Shape. Invocations on a Shape are intercepted by a Shape Synchronization Interface that can let them
1 For
2
objects of the same or different classes.
Client Object
require, preGuard and postGuard. For validation purposes, these operations may use their local synchronization data, the synchronization data of other invocations (contained in other Synchronization Predicate objects) and the object synchronization data (contained in object Synchronization Data). Operations preGuard and postGuard verify whether an invocation is compatible with other concurrent invocations while operation require controls access according to the object state. Operations pre, exec, post, commit and abort update synchronization data.
Functional Object
Synchronization Interface
m’()
m()
Synchronization Data
Synchronizer preControl() postControl()
Synchronization Predicate status invocations
*
require() preGuard() postGuard() pre() exec() post() commit() abort()
Synchronization Data. Provides global object synchronization data. It may use the Functional Object to get synchronization data.
6. Collaborations Figure 2. Object Synchronizer Pattern Structure.
The UML sequence diagram in Figure 3 illustrates collaborations between objects involved in the Object Synchronizer pattern.
Client Object. Requires a service from Functional Object, by invoking one of its operations through Synchronization Interface.
a Synchronization Interface
a Synchronizer
a Functional Object
m()
Functional Object. Contains the functionality code and data. Accesses to it should be synchronized.
CREATE
new()
Synchronization Interface. Is responsible for the synchronization of invocations to the Functional Object using the services provided by the Synchronizer. It creates a Synchronization Predicate object for each invocation. It invokes preControl before invocation proceeds on the Functional Object and postControl after.
a Synchronization Predicate
pre() *{xStatus==DELAY} xStatus = preControl()
PRE-CONTROL
xStatus = require() [xStatus==CONTINUE] xStatus = preGuard() [xStatus==CONTINUE] exec()
POST-CONTROL
EXECUTION
[xStatus==ERROR] abort()
Synchronizer. It decides whether an invocation may continue, stop or should be delayed (returns values CONTINUE, ERROR and DELAY). Operations preControl and postControl control the order of invocations. The former enforces pessimistic synchronization policies while the latter enforces optimistic synchronization policies.
[xStatus==CONTINUE] m’() *{xStatus==DELAY} xStatus = postControl()
xStatus = postGuard() [xStatus==DELAY] post() [xStatus==CONTINUE] commit() [xStatus==ERROR] abort()
Figure 3. Object Synchronizer Pattern Collaborations.
Synchronization Predicate. Identifies the invocation and contains its current status which can be: pre-pending, executing, post-pending, committed and aborted (attribute status with values PRE, EXEC, POST, COMMIT and ABORT). It also contains a queue of predicates: invocations. Defines the synchronization semantics of an invocation through operations
Four collaboration phases are described: 1. CREATE. Corresponds to an access made by a Client object. This phase creates a Synchronization Predicate object. Its status is initialized to PRE. Operation pre may update policy-specific synchro3
Separates functionality code from synchronization code, allowing separate development, test and reuse. Synchronization code is encapsulated by classes Synchronizer, Synchronization Predicate and Synchronization Data.
nization data with information about the invocation category and its argument values. 2. PRE-CONTROL. This phase synchronizes the invocation before accessing the Functional Object (operations require and preGuard). An ERROR value may be returned, preventing the execution of the client invocation, otherwise execution is delayed or resumed. If CONTINUE or ERROR values are returned, the Synchronization Predicate status is updated to, respectively, EXEC or ERROR by operations exec and abort. These operations may also update policy-specific synchronization data in Synchronization Data and Synchronization Predicate objects. This phase is repeated while preControl returns DELAY.
Abstracts several synchronization policies, like readers/writers, synchronization counters, or dynamic priority. The policies can be either optimistic or pessimistic. This extensibility is achieved by specializing classes Synchronizer, Synchronization Predicate and Synchronization Data. This pattern has the following disadvantage:
Increases the number of classes and objects. More classes and objects are needed than in a solution based on synchronization mechanisms as semaphores. However, due to code tangling, this solution is more complex and error prone.
3. EXECUTION. The invocation executes on the Functional Object. This phase occurs only if the previous phase returned CONTINUE.
8. Implementation There are several ways to implement Object Synchronizer pattern. This section discusses major issues and possibilities.
4. POST-CONTROL. This phase verifies if an invocation already done (in state EXEC or POST), is correctly synchronized with other concurrent invocations. The verification protocol (operation postGuard) is similar to that of the previous phase. Operations post, commit, and abort, on the Synchronization Predicate object, set the status value to POST, COMMIT, and ABORT respectively. Additionally, they may update policy-specific synchronization data in Synchronization Data and other Synchronization Predicate objects. This phase is repeated while postControl returns DELAY.
8.1. Customization of Policies Programmers customize synchronization policies by defining specific subclasses of Synchronizer, Synchronization Predicate and Synchronization Data. Pessimistic and Optimistic Policies. Synchronization policies can use two different generic algorithms: pessimistic, when the object is expected to have high contention; and optimistic, when the level of contention is expected to be low. These two perspectives are coded in subclasses of Synchronizer as illustrated in Figure 4. Pessimistic policies control invocations during the PRE-CONTROL phase by verifying compatibility with other Synchronization Predicate objects (operations require and preGuard). During POST-CONTROL, pessimistic policies do not verify compatibility, since synchronizations were already verified (thus operation postGuard is not invoked). Optimistic policies do not control invocations during the PRE-CONTROL phase. Nevertheless, operation require must be invoked to verify whether an object’s state allows invocation execution, e.g. it may not be possible to get a value from an empty buffer. Afterwards, during the POST-CONTROL phase it is necessary to verify if the invocation is compatible with other executing and terminated invocations (operation postGuard).
Operations pre, preControl and postControl must be executed in mutual exclusion since any interference may result in synchronization data corruption.
7. Consequences The Object Synchronizer pattern has the following advantages:
Isolates synchronization code from client objects, allowing the shared object to enforce a consistent synchronization policy. Clients can ignore whether an object is shared or not, which simplifies them. Encapsulation is achieved by placing the synchronization code within the synchronized object such that client objects can invoke the Synchronization Interface ignoring how synchronization is achieved. 4
a Pessimistic Synchronizer
a Optimistic Synchronizer
a Synchronization Predicate
preControl()
Optimistic Readers/Writers. Class and sequence diagrams described in Figure 6 show the specialization of Synchronization Predicate for an optimistic readers/writers synchronization policy.
a Synchronization Predicate
preControl() xStatus = require()
xStatus = require()
[xStatus==CONTINUE] xStatus = preGuard()
[xStatus==CONTINUE] exec()
[xStatus==CONTINUE] exec()
[xStatus==ERROR] abort()
Read/Write Predicate
[xStatus==ERROR] abort()
a Read/Write Predicate
category abort postControl()
postControl()
commit()
getCategory() setAbort()
xStatus = postGuard()
postGuard() ERROR
[xStatus==DELAY] post()
[category==READ]
[xStatus==CONTINUE] commit()
[category==WRITE]
Read Predicate
CONTINUE
[abort] [NOT abort]
Write Predicate
[xStatus==ERROR] abort()
a Read Predicate
a Read/Write Predicate
commit()
Figure 4. Pessimistic and Optimistic Synchronizers.
a Write Predicate
a Read/Write Predicate
commit() *{invocations} s = getStatus() c = getCategory()
*{invocations} s = getStatus() [s==EXEC] setAbort()
[s==EXEC && c==WRITE] setAbort()
Pessimistic Readers/Writers. Class and sequence diagrams presented in Figure 5 show the specialization of Synchronization Predicate for a pessimistic readers/writers synchronization policy.
Figure 6. Optimistic Readers/Writers Policy. Operation postGuard of Read/Write Predicate objects returns ABORT if the abort attribute has the value TRUE. Operation commit of Read Predicate sets the attribute abort of all Write Predicate objects whose status is EXEC to TRUE. Operation commit of Write Predicate sets the attribute abort of all predicate objects whose status is EXEC to TRUE. We assume that each access to the shared object is done in a private copy. If afterwards the invocations terminates with success, we actualize the shared object. This corresponds to the deferred-update recovery policy of the Object Recovery pattern [5].
Read/Write Predicate category getCategory()
[category==READ] Read Predicate
a Read Predicate
preGuard()
a Read/Write Predicate
*{invocations}
[category==WRITE] Write Predicate
a Write Predicate
preGuard()
s = getCategory() DELAY DELAY
*{invocations} s = getStatus()
s = getStatus()
[s==EXEC && c==WRITE]
a Read/Write Predicate
[s===EXEC]
Dynamic Priority Readers/Writers. The pessimistic readers/writers policy allows starvation of writers. To solve this problem, whenever a read starts executing, the priority of pending write invocations is incremented. Class and sequence diagrams presented in Figure 7 show the specialization of Synchronization Predicate for a dynamic priority readers/writers synchronization policy. Operation preGuard of Read Predicate returns DELAY if there is a Write Predicate with MAX priority and operation exec of Read Predicate increments the priority of Write Predicate.
CONTINUE
CONTINUE
Figure 5. Pessimistic Readers/Writers Policy. This solution distinguishes between read and write invocations. A generic synchronization predicate Read/Write Predicate defines the invocation categories, e.g. READ and WRITE. Operation preGuard of Read Predicate returns DELAY if one of the invocations’ predicates is a Write Predicate with status value EXEC, i.e., there is another client invocation that is accessing the shared object in write mode. Operation preGuard of Write Predicate returns DELAY if there is an invocations’ predicate with status value EXEC.
Producer/Consumer. Class and sequence diagrams presented in Figure 8 show the specialization of Synchronization Predicate and Synchronization Data for a producer/consumer synchronization policy. 5
Read/Write Predicate
a Read Predicate
category
Specializations of class Synchronization Data can implement synchronization counters using an attribute for each counter. Moreover, it is necessary that operations pre, exec, post, commit and abort of class Synchronization Predicate invoke the Synchronization Data to update the attribute values accordingly. Note that the previously described readers/writers policies could have been written using synchronization counters.
a Read/Write Predicate
getCategory() [category==READ]
[category==WRITE]
Read Predicate
exec()
*{invocations} s = getStatus()
Write Predicate
c = getCategory()
priority
[s==PRE && c==WRITE] incPriority()
getPriority() incPriority()
a Read Predicate
preGuard()
a Read/Write Predicate
a Write Predicate
*{invocations}
preGuard()
*{invocations}
s = getStatus()
s = getStatus()
c = getCategory()
c = getCategory()
[c==WRITE] p = getPriority()
[c==WRITE] p = getPriority()
[(s==EXEC && c==WRITE) || (s==PRE && c==WRITE && p==MAX)]
DELAY
a Read/Write Predicate
8.2. Separation of Functional and Synchronization Variables
[(s==EXEC) || (s==PRE && c==WRITE && prioritypreControl(pred); // end mutual exclusion mutex_.release(); // if there are incompatibilities wait if (status == DELAY) condition_.wait(); } while (status == DELAY);
Concurrent Object. A mutex object is used to support mutual exclusion of synchronization code. A condition object is used to block the activity associated with an invocation if DELAY is returned. Active Object. The design of an active object is described in [7]. It contains a queue of method objects representing pending invocations. A method object has the code associated with an invocation. Active objects use a scheduler object to select and execute pending method objects.
if (status == CONTINUE) // invocation proceeds on functional object shape_ shape_->moveBy(delta); else return;
Mutual exclusion of synchronization code is provided by the internal scheduler thread. Activity delay is implemented by moving the invocation to the end of the queue.
// post-control do { // begin mutual exclusion mutex_.acquire();
7
{
// verify compatibility status = synchronizer_->postControl(pred); // end mutual exclusion mutex_.release(); if (status == DELAY) condition_.wait(); else // only if CONTINUE or ERROR // awake pending invocations condition_.broadcast();
// iterator for predicates PIterator iter(sync_); SynchronizationPredicate *pred; while (pred = iter.next(), pred != 0) // there are invocations executing if ((pred->getStatus() == EXEC)) // conflict return DELAY; // no conflict return CONTINUE;
} while (status == DELAY);
}
} // requires that invoker is shape’s owner X_Status MoveByPredicate::require() { // InvOwner global function returns invoker id // sd_ is an instance of a Synchronization Data // subclass which contains the shape’s owner if (InvOwner() == sd_->owner()) return CONTINUE;
Note that, since pre-control and post-control code fragments are independent of a particular invocation it could be defined as two protected operations of class Synchronization Interface. For each synchronized operation, these generic operations could be invoked receiving a Synchronization Predicate object as argument. Particular synchronization policies are defined by classes MoveBy Predicate and GetData Predicate which are subclasses of Shape Predicate. Shape Predicate inherits from Synchronization Predicate and defines two invocation categories: MOVE and GET.
return ERROR; }
A publicly writable shape allows any user to move the shape. The synchronization predicates are identical to publicly readable shape’s synchronization predicates except for predicate MoveBy Predicate which has to relax the require operation. // publicly writable moveBy require // any user can move the shape X_Status MoveByPredicate::require() { return CONTINUE; }
// Generic shape predicate class ShapePredicate : public SynchronizationPredicate { public: ShapePredicate(int cat) : cat_(cat) {} ˜ShapePredicate() {} // returns category int getCat() { return cat_; } // other operations to be redefined // ... private: // Invocation categories: MOVE and GET CAT cat_; };
10. Known Uses The need for object synchronization is widely recognized in concurrent programming languages, objectoriented databases, and distributed object systems. Most solutions to this problem restrict the supported number of policies and do not decouple synchronization from concurrency. Distributed systems, e.g. Arjuna [8] and Hermes/ST [9], use the Object Synchronizer pattern encapsulated by platform mechanisms. Arjuna defines two classes, Lock and LockManager: class LockManager supports a pessimistic synchronization policy while class Lock contains object-specific information. Programmers only need to redefine Lock operations. In Hermes there are two kinds of synchronization: implicit and explicit. Implicit synchronization offers a transparent pessimistic policy to synchronize invocations with attribute granularity, while explicit synchronization uses the object’s state. Explicit synchronization defines class ProgrammableLock which has two operations: isScheduable and isCompatibleWith. The former uses the object’s state while the latter defines the compatibility between operations. Scheduling predicates were defined in [10] in the context of a concurrent object-oriented language. This language
A publicly readable shape defines GetData Predicate to allow concurrent execution of getData invocations. Class MoveBy Predicate restricts invocations of moveBy to the shape’s owner and forbids concurrent execution of moveBy invocations. Operation getData is a read operation while moveBy is a private write operation. // prevents conflicts with executing moveBy X_Status GetDataPredicate::preGuard() { // iterator for predicates PIterator iter(sync_); SynchronizationPredicate *pred; while (pred = iter.next(), pred != 0) // there are moveBy invocations executing if ((pred->getStatus() == EXEC) && (((ShapePredicate*)pred)->getCat() == MOVE)) // conflict return DELAY; // no conflict return CONTINUE; } // prevents conflicts with executing getData and moveBy X_Status MoveByPredicate::preGuard()
8
References
contains identical abstractions and synchronization expressibility as Object Synchronizer pattern. The Object Synchronizer pattern is integrated with other design patterns: Object Concurrency and Object Recovery [5]. It is implemented as part of an object-oriented framework that supports object concurrency, synchronization and recovery [11]. This framework is publicly available from http://www.esw.inesc.pt/˜ars/dasco.
[1] E. W. Dijkstra. Cooperating Sequential Processes. In F. Genuys, editor, Programming Languages. Academic Press, 1968. [2] C. A. R. Hoare. Monitors: An Operating System Structuring Concept. Comunications of the ACM, 17(10), October 1974. [3] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable ObjectOriented Software. Addison Wesley, 1994.
11. Related Patterns
[4] Martin Fowler and Kendall Scott. UML Destilled: Applying de Standard Object Modeling Language. Addison-Wesley, 1997.
The Active Object pattern [7] decouples operation execution from operation invocation in order to simplify synchronized accesses to a shared resource. The active object has an internal thread which dispatches pending operations. The internal thread can do some synchronization when selecting the next operation to dispatch. The Object Synchronizer abstracts synchronization policies independently of a particular implementation of object concurrency such as active object. The Object Recovery pattern [5] abstracts several policies for object recovery. Combined with the Object Synchronizer pattern it allows implementation of synchronization policies that need to recover the object’s state, e.g. optimistic policies. However, some combinations of synchronization and recovery policies are not possible or, although possible, penalize performance. It has been proven that some compatibility relations between invocations require a particular kind of recovery policy [12], e.g. forward commutativity requires an update-in-place recovery policy. Common combinations are optimistic policies and deferredupdate recovery policies, where simultaneous invocations proceed on a copy of the object (optimized abort), and pessimistic with update-in-place recovery policies (optimized commit), where invocations proceed on the same object. The Proxy pattern [3] is used to control accesses to the Functional Object. The Functional Object corresponds to RealSubject while the Synchronization Interface corresponds to Proxy. The Functional Object does not know anything about the Synchronization Interface. The Strategy pattern [3] is used between the Synchronization Interface and the Synchronizer. It provides the configuration of Synchronization Interface with synchronization policies.
[5] Ant´onio Rito Silva, Joao Pereira, and Jos´e Alves Marques. Object Recovery. In Robert Martin, Dirk Riehle, and Frank Buschman, editors, Pattern Languages of Program Design 3, chapter 15, pages 261–276. Addison-Wesley, 1997. [6] D. Decouchant, P. Le Dot, M. Riveill, C. Roisin, and X. Rousset de Pina. A synchronization mechanism for an object-oriented distributed system. In Proceedings of the 11th International Conference on Distributed Computing Systems, pages 152–159, Arlington, Texas, USA, May 1991. [7] R. Greg Lavender and Douglas C. Schmidt. Active Object: an Object Behavioral Pattern for Concurrent Programming. In John M. Vlissides, James O. Coplien, and Norman L. Kerth, editors, Pattern Languages of Program Design 2, pages 483–499. Addison-Wesley, 1996. [8] Santosh K. Shrivastava, Graeme N. Dixon, and Graham D. Parrington. An overview of the arjuna distributed programming system. IEEE Software, pages 66–33, January 1991. [9] Michael Fazzolare, Bernhard G. Humm, and R. David Ranson. Concurrency control for distributed nested transactions in hermes. International Conference for Concurrent and Distributed Systems, 1993. [10] Ciaran McHale, Bridget Walsh, Sean Baker, and Alexis Donnelly. Scheduling Predicates. In Mario Tokoro, Oscar Nierstrasz, and Peter Wegner, editors, Proceedings of the ECOOP’91 Workshop on Object-based Concurrent Computing, volume 612, pages 177–193. Springer-Verlag, 1991. [11] Ant´onio Rito Silva. Development and Extension of a ThreeLayered Framework. In Saba Zamir, editor, Handbook of Object Technology, chapter 27. CRC Press, 1998. [12] William Weihl. The Impact of Recovery in Concurrency Control. Journal of Computer and System Sciences, 47(1):157–184, August 1993.
Acknowledgments. Thanks to our colleagues Pedro Sousa, David Matos, Lu´ıs Gil and Jo˜ao Martins. We also thank the participants of the EuroPLoP’96 writers workshop on Distribution. 9