Synchronizing Access to an Object

Problem

You want to make an object accessible from only one thread at a time.

Solution

Give the object a Mutex member (a semaphore that controls whose turn it is to use the object). You can then use this to synchronize activity on the object.

This code gives every object a synchronize method. This simulates the behavior of Java, in which synchronize is a keyword that can be applied to any object:

require hread class Object def synchronize mutex.synchronize { yield self } end def mutex @mutex ||= Mutex.new end end

Heres an example. The first thread gets a lock on the list and then dawdles for a while. The second thread is ready from the start to add to the list, but it doesn get a chance until the first thread releases the lock.

list = [] Thread.new { list.synchronize { |l| sleep(5); 3.times { l.push "Thread 1" } } } Thread.new { list.synchronize { |l| 3.times { l.push "Thread 2" } } } sleep(6) list # => ["Thread 1", "Thread 1", "Thread 1", "Thread 2", "Thread 2", "Thread 2"]

Object#synchronize only prevents two synchronized code blocks from running at the same time. Nothing prevents a wayward thread from modifying the object without calling synchronize first:

list = [] Thread.new { list.synchronize { |l| sleep(5); 3.times { l.push "Thread 1" } } } Thread.new { 3.times { list.push "Thread 2" } } sleep(6) list # => ["Thread 2", "Thread 2", "Thread 2", "Thread 1", "Thread 1", "Thread 1"]

Discussion

One of the big advantages of multithreaded programs is that different threads can share data. But where there is data sharing, there is the possibility for corruption. When two threads operate on the same object at the same time, the results can vary wildly depending on when the Ruby interpreter decides to switch between threads. To get predictable behavior, you need to have one thread lock the object, so other threads can use it.

When every object has a synchronize method, its easier to share an object between threads: if you want to work alone with the object, you put that code within a synchronize block. Of course, you may find yourself constantly writing synchronization code whenever you call certain methods of an object.

It would be nice if you could to do this synchronization implicitly, the way you can in Java: you just designate certain methods as "synchronized," and the interpreter won start running those methods until it can obtain an exclusive lock on the corresponding object. The simplest way to do this is to use aspect-oriented programming. The RAspect library described in Recipe 10.15 can be used for this.

The following code defines an Aspect that can wrap methods in synchronization code. It uses the Object#mutex method defined above, but it could easily be changed to define its own Mutex objects:

require aspectr require hread class Synchronized < AspectR::Aspect def lock(method_sym, object, return_value, *args) object.mutex.lock end def unlock(method_sym, object, return_value, *args) object.mutex.unlock end end

Any AspectR aspect method needs to take three arguments: the symbol of the method being called, the object its being called on, and (if the aspect method is being called after the original method) the return value of the method.

The rest of the arguments are the arguments to the original method. Since this aspect is very simple, the only argument we need is object, the object we e going to lock and unlock.

Lets use the Synchronized aspect to create an array where you can only call push, pop, or each once you get an exclusive lock.

array = %w{do re mi fa so la ti} Synchronized.new.wrap(array, :lock, :unlock, :push, :pop, :each)

The call to wrap tells AspectR to modify our arrays implementation of push, pop, and each with generated singleton methods. Synchronized#lock is called before the old implementation of those methods is run, and Synchronized#unlock is called afterward.

The following example creates two threads to work on our synchronized array. The first thread iterates over the array, and the second thread destroys its contents with repeated calls to pop. When the first thread calls each, the AspectR-generated code calls lock, and the first thread gets a lock on the array. The second thread starts and it wants to call pop, but pop has been modified to require an exclusive lock on the array. The second thread can run until the first thread finishes its call to each, and the AspectR-generated code calls unlock.

Thread.new { array.each { |x| puts x } } Thread.new do puts Destroying the array. array.pop until array.empty? puts Destroyed! end # do # re # mi # fa # so # la # ti # Destroying the array. # Destroyed!

See Also

Категории