The Pragmatic Programmers
Programming Ruby

Programming Ruby

 
  Home
  Programming Ruby
  Links to Ruby Information
  Class reference
  Articles on Ruby
      Ruby and the Web
      Distributed Ruby
      Calling Methods Dynamically
      Blocks and Constructors
»     Mutual Exclusion
      Messing Around with Objects
      Embedded Documentation
      OO Regular Expressions
      Test-first in Ruby
  Downloads
  Errata
  Code from the book
  New Features
  Presentation: Intro. to Ruby

Mutual Exclusion


Ruby's built-in threads are both useful and easy to use. However, if your threads need to communicate, or they need to share data or other resources, some form of synchronization will be needed.

The lowest-level method to block other threads from running is Thread#critical= , which sets a global ``thread critical'' condition. When set to true, the scheduler will not schedule any existing thread to run. However, this does not block new threads from being created and run. Certain thread operations (such as stopping or killing a thread, sleeping in the current thread, or raising an exception) may cause a thread to be scheduled even when in a critical section.

Using #critical= directly is certainly possible, but it isn't terribly convenient. Fortunately, Ruby comes packaged with several alternatives. Of these, two of the best, class Mutex and class ConditionalVariable are available in the thread library module.

The Mutex Class

Mutex is a class which implements a simple semaphore lock for mutually exclusive access to some shared resource. That is, only one thread may hold the lock at a given time. Other threads may choose to wait in line for the lock to become available, or may simply choose to get an immediate error indicating that the lock is not available.

A mutex is often used when updates to shared data need to be atomic. Say we need to update two variables as part of a transaction. We can simulate this in a trivial program by incrementing some counters. The updates are supposed to be atomic---the outside world should never see the counters with different values. Without any kind of mutex control, this just doesn't work.
  
  count1 = 0
  count2 = 0
  difference = 0
  counter = Thread.new do
    loop do
      count1 += 1
      count2 += 1
    end
  end
  spy = Thread.new do
    loop do
      difference += (count1 - count2).abs
    end
  end
  sleep 1        # -> 1
  Thread.critical = 1
  count1         # -> 200720
  count2         # -> 200719
  difference     # -> 90600

This example shows that the ``spy'' thread woke up a large number of times and found the values of count1 and count2 inconsistent.

Fortunately we can fix this using a mutex.
  
  require 'thread'
  mutex = Mutex.new
  
  count1 = count2 = 0
  difference = 0
  counter = Thread.new do
    loop do
      mutex.synchronize do
        count1 += 1
        count2 += 1
      end
    end
  end
  spy = Thread.new do
    loop do
      mutex.synchronize do
        difference += (count1 - count2).abs
      end
    end
  end
  sleep 1      
  mutex.lock   
  count1         # -> 18403
  count2         # -> 18403
  difference     # -> 0

By placing all accesses to the shared data under control of a mutex, we ensure consistency. Unfortunately, as you can see from the numbers, we also experience quite a performance penalty.

Condition Variables

Using a mutex to protect critical data is sometimes not enough. Suppose you are in a critical section, but you need to wait for some particular resource. If your thread goes to sleep waiting for this resource, it is possible that no other thread will be able to release the resource as they cannot enter the critical section---the original process still has it locked. You need to be able to temporarily give up your exclusive use of the critical region and simultaneously tell people that you're waiting for a resource. When the resource comes available, you need to be able to grab it and re-obtain the lock on the critical region, all in one step. This is where condition variables come in. A condition variable is simply a semaphore which is associated with a resource, and which is used within the protection of a particular mutex. When you need a resource that's unavailable, you wait on a condition variable. That action releases the lock on the corresponding mutex. When some other thread signals that the resource is available, the original thread comes off the wait, and simultaneously regains the lock on the critical region.
  
  require 'thread'
  mutex = Mutex.new
  cv = ConditionVariable.new

  a = Thread.new {
    mutex.synchronize {
      puts "A: I have critical section, but will wait for cv"
      cv.wait(mutex)
      puts "A: I have critical section again! I rule!"
    }
  }

  puts "(Later, back at the ranch...)"

  b= Thread.new {
    mutex.synchronize {
      puts "B: Now I am critical, but am done with cv"
      cv.signal
      puts "B: I am still critical, finishing up"
    }
  }
  a.join
  b.join

Produces:

A: I have critical section, but will wait for cv(Later, back at the ranch...)

B: Now I am critical, but am done with cv
B: I am still critical, finishing up
A: I have critical section again! I rule!

For alternative implementations of synchronization mechanisms, see monitor.rb and sync.rb in the lib subdirectory of the distribution.


Copyright (c) 2001, The Pragmatic Programmers

 

  Home      PragProg      Ruby      Contact us