Pattern: Process Safe Class

Context

Some objects need to be shared between multiple independent processes, and protected so that attempts to mutate the object from one process do not impinge on the attempts to access or mutate the object from other processes. Access to the state of the object must be synchronised so that corruption and dirty reads do not occur.

Solution

  1. Create a suitable new class (New Class). The classes instances must be a pointer objects. Where a process safe byte object is required, implement a wrapper class containing the byte object as an instance variable (Adapter). Often you will be subclassing an existing, non-process safe, class (for example, to create a process safe collection from one of the standard Collection classes).
  2. Add an instance variable called mutex to the class. This instance variable will hold the process synchronization object.
  3. Assign a new instance of the Mutex class to the mutex instance variable in the instance #initialize method. Mutexes are preferable to Semaphores for the task of creating a process safe subclass of a class not designed to be so, because they permit multiple entries to a critical section from the same process, while excluding other processes. This property means we do not have to worry about a process deadlocking itself.
  4. Superclass methods should be overridden to perform a supersend of the same message to the superclass, but inside a critical section guarded by the mutex, (see the example).
  5. Selectively override private superclass methods which are used as accessors by the public methods. Where the superclass is neatly implemented, protecting these private accessors may make protecting a number of public methods unnecessary.
  6. Override all public superclass methods which directly access the shared data of the superclass where those methods are not made process safe by virtue of carrying out all critical operations using previously protected private methods. 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.
  7. Don't forget methods inherited from ther superclass' superclass, and so on.
  8. Selectively override private superclass methods which are used as entry points from other related classes.

Example

A relatively simple example in the base system is SharedSet (a process safe subclass of Set). New SharedSets are initialized as follows:

initialize
	"Instance variable initialization. The mutex protects against concurrent access from multiple
	processes, but permits the same process to make multiple entries."

	super initialize.
	mutex := Mutex new

The Set>>add: method is overridden as follows:

add: newObject
	"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:

do: operation
	"Evaluate monadic value argument, operation, for each of the elements (non-nil members) 
	of the receiver. Answers the receiver.
	N.B. It is important that operation does not put the active process to sleep (i.e.
	wait on some Semaphore) as this will prevent other Smalltalk processes from accessing
	the receiver for a potentially long time, and if a weak subclass, may prevent the
	removal of Corpses from taking place. In the case of weak subclasses, if the putting the
	active process to sleep is unavoidable, then the weak status should be removed until
	the end of the critical block (e.g. send #beStrong at the start of the block, and
	#beWeakWithNotify at the end of the block)."

	mutex critical: [super do: operation]

SharedSet conservatively overrides #asArray as follows:

asArray
	"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.

asArray
	"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].
	^anArray

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]
		ifFalse: [answer]

SharedSet overrides other methods as necessary - see the documentation method #overrideStrategy for explanation of why certain methods are overridden, and others not.

Known Uses

Forces

Related Patterns