Navigation: Appendix B: Dolphin Pattern Book > Class Patterns >
Process Safe Class
You are creating a New Class and are aware that some instances of this class will need to be shared between multiple independent Smalltalk processes. In such cases, the class must be protected so that attempts to mutate an instance from one process do not impinge on the attempts to access or mutate the instance from other processes. How do we control and synchronize access to the state of the instances so that corruption does not occur?
Create a Process Safe Class that contains a mutex to control access to the critical state of the classes instances. Step by step instructions are as follows:
|1.||Create a suitable New Class and ensure that it is a pointer object (not byte object) class. If a process safe byte object is required, implement a wrapper class containing the byte object as an instance variable (see Inheritance vs Composition).|
|2.||Add an instance variable, mutex, to hold the process synchronization object and during Instance Initialization assign a new instance of Mutex to this variable. A Mutex is preferable to a Semaphore because it permits multiple entries to a critical section from the same process, while excluding only other processes. This means that you not have to worry about a single process deadlocking itself.|
|3.||All the superclass methods that provide access to the shared state must be overridden to perform a supersend of the same message to the superclass but inside a critical section guarded by the mutex.|
|•||One way of doing this neatly is to selectively override private superclass methods which are used as accessors by the class's public methods. Protecting these private accessors may make protecting a number of public methods unnecessary.|
|•||Override all public superclass methods which directly access the shared data of the superclass where those methods are not made process safe by the above.|
|•||If a particular method involves a number of operations which should be carried out indivisibly, then that method must be overridden and protected by the mutex, regardless of whether the implementation uses protected private methods.|
|4.||Don't forget to protect methods inherited from the superclass' superclass, and so on.|
A relatively simple example in the base system is SharedSet (a process safe subclass of Set). New SharedSets are initialized as follows:
"Instance variable initialization. The mutex protects against concurrent access from multiple
processes, but permits the same process to make multiple entries."
mutex := Mutex new
The Set>>add: method is overridden as follows:
"Include newObject as one of the elements of the receiver. Answer newObject."
^mutex critical: [super add: newObject]
Note that the value of the critical section is the value of the expression inside the block, so there is no need to perform a ^-return inside the critical section and cause an unwind to be set in motion.
#do: is overridden in a similar manner:
"Evaluate monadic value argument, operation, for each of the
elements (non-nil members) of the receiver. Answers the receiver."
mutex critical: [super do: operation]
SharedSet conservatively overrides #asArray as follows:
"Answer an Array whose elements are those of the receiver
(ordering is possibly arbitrary). Must implement as critical section
as otherwise Array size might be wrong."
^mutex critical: [super asArray]
This is apparently protected by the overridden #do: method, but the size of the array could turn out to be wrong if resized by another process during the execution of Collection>>asArray (below) between the point where the size of the SharedSet is taken, and the #do: message being sent.
"Answer an Array whose elements are those of the receiver.
(ordering is that of the #do: operation as implemented by the receiver)."
| anArray i |
anArray := Array new: self size.
i := 1.
self do: [:e |
anArray at: i put: e.
i := i + 1].
To avoid the possibility of a client supplied "if absent" block raising an exception inside a critical section, SharedSet>>remove:ifAbsent: makes use of a unique object (or cookie) to identify the "not found" case, and then evaluates the client supplied exception handler when the cookie is detected, as follows:
remove: oldElement ifAbsent: exceptionHandler
"If oldElement is one of the receiver's elements, then remove it from the
receiver and answer it (as Sets cannot contain duplicates, only one element is
ever removed). If oldElement is not an element of the receiver (i.e.
no element of the receiver is #= to oldObject) then answer the
result of evaluating the niladic valuable, exceptionHandler."
| answer |
answer := mutex critical: [super remove: oldElement ifAbsent: [AbsentCookie]].
^answer == AbsentCookie
ifTrue: [exceptionHandler value]
SharedSet overrides other methods as necessary - see the documentation method #overrideStrategy for explanation of why certain methods are overridden, and others not.
Most Weak Collections are subclasses of process safe classes (e.g. WeakLookupTable is a subclass of SharedLookupTable).
|•||If your requirements are met by an existing shared collection, then it is easier to make use of those by containment rather than creating your own new process safe subclass.|
|•||Tracking down bugs resulting from process synchronization problems can be very difficult, so it is better to be cautious, and protect too much rather than too little.|
|•||Process safety must be maintained in the presence of future changes, so spare some thought to the ease with which future maintenance can be carried out.|
|•||The objective is to achieve mutually exclusive access to the shared state of the object; don't try to be too clever.|
|•||Performing explicit ^-returns and raising exceptions from inside the critical section is possible, but incurs some unwind overhead to ensure that the mutex is unlocked, and will also increase the chance of a deadlock occurring.|