Threads and Concurrency

Traditional programs have a single “thread of execution”: the statements or instructions that comprise the program are executed sequentially until the program terminates. A multithreaded program has more than one thread of execution. Within each thread, statements are executed sequentially, but the threads themselves may be executed in parallel—on a multicore CPU, for example. Often (on single-core, single-CPU machines, for instance), multiple threads are not actually executed in parallel, but parallelism is simulated by interleaving the execution of the threads.

Programs such as image processing software that perform a lot of calculations are said to be compute-bound. They can only benefit from multithreading if there are actually multiple CPUs to run computations in parallel. Most programs are not fully compute-bound, however. Many, such as web browsers, spend most of their time waiting for network or file I/O. Programs like these are said to be IO-bound. IO-bound programs can be usefully multithreaded even when there is only a single CPU available. A web browser might render an image in one thread while another thread is waiting for the next image to be downloaded from the network.

Ruby makes it easy to write multi-threaded programs with the Thread class. To start a new thread, just associate a block with a call to Thread.new. A new thread will be created to execute the code in the block, and the original thread will return from Thread.new immediately and resume execution with the next statement:

# Thread #1 is running here
Thread.new {
  # Thread #2 runs this code
}
# Thread #1 runs this code

We’ll begin our coverage of threads by explaining Ruby’s thread model and API in some detail. These introductory sections explain things such as thread lifecycle, thread scheduling, and thread states. With that introductory material as prerequisite, we move on to present example code and to cover advanced topics such as thread synchronization.

Finally, it is worth noting that Ruby programs can also achieve concurrency at the level of the operating system process by running external executables or by forking new copies of the Ruby interpreter. Doing this is operating system-dependent, however, and is covered only briefly in Chapter 10. For further information, use ri to look up the methods Kernel.system, Kernel.exec, Kernel.fork, IO.popen, and the module Process.

Thread Lifecycle

As described above, new threads are created with Thread.new. You can also use the synonyms Thread.start and Thread.fork. There is no need to start a thread after creating it; it begins running automatically when CPU resources become available. The value of the Thread.new invocation is a Thread object. The Thread class defines a number of methods to query and manipulate the thread while it is running.

A thread runs the code in the block associated with the call to Thread.new and then it stops running. The value of the last expression in that block is the value of the thread, and can be obtained by calling the value method of the Thread object. If the thread has run to completion, then the value returns the thread’s value right away. Otherwise, the value method blocks and does not return until the thread has completed.

The class method Thread.current returns the Thread object that represents the current thread. This allows threads to manipulate themselves. The class method Thread.main returns the Thread object that represents the main thread—this is the initial thread of execution that began when the Ruby program was started.

The main thread

The main thread is special: the Ruby interpreter stops running when the main thread is done. It does this even if the main thread has created other threads that are still running. You must ensure, therefore, that your main thread does not end while other threads are still running. One way to do this is to write your main thread in the form of an infinite loop. Another way is to explicitly wait for the threads you care about to complete. We’ve already mentioned that you can call the value method of a thread to wait for it to finish. If you don’t care about the value of your threads, you can wait with the join method instead.

The following method waits until all threads, other than the main thread and the current thread (which may be the same thing), have exited:

# Wait for all threads (other than the current thread and
# main thread) to stop running.
# Assumes that no new threads are started while waiting.
def join_all
  main = Thread.main        # The main thread
  current = Thread.current  # The current thread
  all = Thread.list         # All threads still running
  # Now call join on each thread
  all.each {|t| t.join unless t == current or t == main }
end

Threads and unhandled exceptions

If an exception is raised in the main thread, and is not handled anywhere, the Ruby interpreter prints a message and exits. In threads other than the main thread, unhandled exceptions cause the thread to stop running. By default, however, this does not cause the interpreter to print a message or exit. If a thread t exits because of an unhandled exception, and another thread s calls t.join or t.value, then the exception that occurred in t is raised in the thread s.

If you would like any unhandled exception in any thread to cause the interpreter to exit, use the class method Thread.abort_on_exception=:

Thread.abort_on_exception = true

If you want an unhandled exception in one particular thread to cause the interpreter to exit, use the instance method by the same name:

t = Thread.new { ... }
t.abort_on_exception = true

Threads and Variables

One of the key features of threads is that they can share access to variables. Because threads are defined by blocks, they have access to whatever variables (local variables, instance variables, global variables, and so on) are in the scope of the block:

x = 0

t1 = Thread.new do
  # This thread can query and set the variable x
end

t2 = Thread.new do
  # This thread and also query and set x
  # And it can query and set t1 and t2 as well.   
end

When two or more threads read and write the same variables concurrently, they must be careful that they do so correctly. We’ll have more to say about this when we consider thread synchronization below.

Thread-private variables

Variables defined within the block of a thread are private to that thread and are not visible to any other thread. This is simply a consequence of Ruby’s variable scoping rules.

We often want a thread to have its own private copy of a variable so that its behavior does not change if the value of that variable changes. Consider the following code, which attempts to create three threads that print (respectively) the numbers 1, 2, and 3:

n = 1
while n <= 3
  Thread.new { puts n }
  n += 1
end 

In some circumstances, in some implementations, this code might work as expected and print the numbers 1, 2, and 3. In other circumstances or in other implementations, it might not. It is perfectly possible (if newly created threads do not run right away) for the code to print 4, 4, and 4, for example. Each thread reads a shared copy of the variable n, and the value of that variable changes as the loop executes. The value printed by the thread depends on when that thread runs in relation to the parent thread.

To solve this problem, we pass the current value of n to the Thread.new method, and assign the current value of that variable to a block parameter. Block parameters are private to the block (but see Blocks and Variable Scope for cautions), and this private value is not shared between threads:

n = 1
while n <= 3
  # Get a private copy of the current value of n in x
  Thread.new(n) {|x| puts x }
  n += 1
end 

Note that another way to solve this problem is to use an iterator instead of a while loop. In this case, the value of n is private to the outer block and never changes during the execution of that block:

1.upto(3) {|n| Thread.new { puts n }}

Thread-local variables

Certain of Ruby’s special global variables are thread-local: they may have different values in different threads. $SAFE (see Security) and $~ (see Table 9-3) are examples. This means that if two threads are performing regular expression matching concurrently, they will see different values of $~, and performing a match in one thread will not interfere with the results of a match performed in another thread.

The Thread class provides hash-like behavior. It defines [] and []= instance methods that allow you to associate arbitrary values with any symbol. (If you use a string instead, it will be converted to a symbol. Unlike true hashes, the Thread class only allows symbols as keys.) The values associated with these symbols behave like thread-local variables. They are not private like block-local variables because any thread can look up a value in any other thread. But they are not shared variables either, since each thread can have its own copy.

As an example, suppose that we’ve created threads to download files from a web server. The main thread might want to monitor the progress of the download. To enable this, each thread might do the following:

Thread.current[:progress] = bytes_received

The main thread could then determine the total bytes downloaded with code like this:

total = 0
download_threads.each {|t| total += t[:progress] }

Along with [] and []=, Thread also defines a key? method to test whether a given key exists for a thread. The keys method returns an array of symbols representing the defined keys for the thread. This code could be better written as follows, so that it works for threads that have not yet started running and have not defined the :progress key yet:

total = 0
download_threads.each {|t| total += t[:progress] if t.key?(:progress)}

Thread Scheduling

Ruby interpreters often have more threads to run than there are CPUs available to run them. When true parallel processing is not possible, it is simulated by sharing a CPU among threads. The process for sharing a CPU among threads is called thread scheduling. Depending on the implementation and platform, thread scheduling may be done by the Ruby interpreter, or it may be handled by the underlying operating system.

Thread priorities

The first factor that affects thread scheduling is thread priority: high-priority threads are scheduled before low-priority threads. More precisely, a thread will only get CPU time if there are no higher-priority threads waiting to run.

Set and query the priority of a Ruby Thread object with priority= and priority. Note that there is no way to set the priority of a thread before it starts running. A thread can, however, raise or lower its own priority as the first action it takes.

A newly created thread starts at the same priority as the thread that created it. The main thread starts off at priority 0.

Like many aspects of threading, thread priorities are dependent on the implementation of Ruby and on the underlying operating system. Under Linux, for example, nonprivileged threads cannot have their priorities raised or lowered. So in Ruby 1.9 (which uses native threads) on Linux, the thread priority setting is ignored.

Thread preemption and Thread.pass

When multiple threads of the same priority need to share the CPU, it is up to the thread scheduler to decide when, and for how long, each thread runs. Some schedulers are preempting, which means that they allow a thread to run only for a fixed amount of time before allowing another thread of the same priority to run. Other schedulers are not preempting: once a thread starts running, it keeps running unless it sleeps, blocks for I/O, or a higher-priority thread wakes up.

If a long-running compute-bound thread (i.e., one that does not ever block for I/O) is running on a nonpreempting scheduler, it will “starve” other threads of the same priority, and they will never get a chance to run. To avoid this issue, long-running compute-bound threads should periodically call Thread.pass to ask the scheduler to yield the CPU to another thread.

Thread States

A Ruby thread may be in one of five possible states. The two most interesting states are for live threads: a thread that is alive is runnable or sleeping. A runnable thread is one that is currently running, or that is ready and eligible to run the next time there are CPU resources for it. A sleeping thread is one that is sleeping (see Kernel.sleep), that is waiting for I/O, or that has stopped itself (see Thread.stop below). Threads typically go back and forth between the runnable and sleeping states.

There are two thread states for threads that are no longer alive. A terminated thread has either terminated normally or has terminated abnormally with an exception.

Finally, there is one transitional state. A thread that has been killed (see Thread.kill below) but that has not yet terminated is said to be aborting.

Querying thread state

The Thread class defines several instance methods for testing the status of a thread. alive? returns true if a thread is runnable or sleeping. stop? returns true if a thread is in any state other than runnable. Finally, the status method returns the state of the thread. There are five possible return values corresponding to the five possible states as shown in the following table.

Thread stateReturn value
Runnable"run"
Sleeping"sleep"
Aborting"aborting"
Terminated normallyfalse
Terminated with exceptionnil

Altering state: pausing, waking, and killing threads

Threads are created in the runnable state, and are eligible to run right away. A thread can pause itself—enter the sleeping state—by calling Thread.stop. This is a class method that operates on the current thread—there is no equivalent instance method, so one thread cannot force another thread to pause. Calling Thread.stop is effectively the same thing as calling Kernel.sleep with no argument: the thread pauses forever (or until woken up, as explained below).

Threads also temporarily enter the sleeping state if they call Kernel.sleep with an argument. In this case, they automatically wake up and reenter the runnable state after (approximately) the specified number of seconds pass. Calling blocking IO methods may also cause a thread to sleep until the IO operation completes—in fact, it is the inherent latency of IO operations that makes threading worthwhile even on single-CPU systems.

A thread that has paused itself with Thread.stop or Kernel.sleep can be started again (even if the sleep time has not expired yet) with the instance methods wakeup and run. Both methods switch the thread from the sleeping state to the runnable state. The run method also invokes the thread scheduler. This causes the current thread to yield the CPU, and may cause the newly awoken thread to start running right away. The wakeup method wakes the specified thread without yielding the CPU.

A thread can switch itself from the runnable state to one of the terminated states simply by exiting its block or by raising an exception. Another way for a thread to terminate normally is by calling Thread.exit. Note that any ensure clauses are processed before a thread exits in this way.

A thread can forcibly terminate another thread by invoking the instance method kill on the thread to be terminated. terminate and exit are synonyms for kill. These methods put the killed thread into the terminated normally state. The killed thread runs any ensure clauses before it actually dies. The kill! method (and its synonyms terminate! and exit!) terminate a thread but do not allow any ensure clauses to run.

The thread termination methods described so far all force a thread to the terminated normally state. You can raise an exception within another thread with the instance method raise. If the thread cannot handle the exception you have imposed on it, it will enter the terminated with exception state. The threads ensure clauses are processed as they would normally be during the course of exception propagation.

Killing a thread is a dangerous thing to do unless you have some way of knowing that the thread is not in the middle of altering the shared state of your system. Killing a thread with one of the ! methods is even more dangerous because the killed thread may leave files, sockets, or other resources open. If a thread must be able to exit upon command, it is better to have it periodically check the state of a flag variable and terminate itself safely and gracefully if or when the flag becomes set.

Listing Threads and Thread Groups

The Thread.list method returns an array of Thread objects representing all live (running or sleeping) threads. When a thread exits, it is removed from this array.

Every thread other than the main thread is created by some other thread. Threads could, therefore, be organized into a tree structure, with every thread having a parent and a set of children. The Thread class does not maintain this information, however: threads are usually considered autonomous rather than subordinate to the thread that created them.

If you want to impose some order onto a subset of threads, you can create a ThreadGroup object and add threads to it:

group = ThreadGroup.new
3.times {|n| group.add(Thread.new { do_task(n) }}

New threads are initially placed in the group to which their parent belongs. Use the instance method group to query the ThreadGroup to which a thread belongs. And use the list method of ThreadGroup to obtain an array of threads in a group. Like the class method Thread.list, the instance method ThreadGroup.list returns only threads that have not terminated yet. You can use this list method to define methods that operate on all threads in a group. Such a method might lower the priority of all threads in the group, for example.

The feature of the ThreadGroup class that makes it more useful than a simple array of threads is its enclose method. Once a thread group has been enclosed, threads may not be removed from it and new threads cannot be added to it. The threads in the group may create new threads, and these new threads will become members of the group. An enclosed ThreadGroup is useful when you run untrusted Ruby code under the $SAFE variable (see Security) and want to keep track of any threads spawned by that code.

Threading Examples

Now that we’ve explained Ruby’s thread model and thread API, we’ll take a look at some actual examples of multithreaded code.

Reading files concurrently

The most common use of Ruby’s threads is in programs that are IO-bound. They allow programs to keep busy even while waiting for input from the user, the filesystem, or the network. The following code, for example, defines a method conread (for concurrent read) that takes an array of filenames and returns a hash mapping those names to the contents of those files. It uses threads to read those files concurrently, and is really intended for use with the open-uri module, which allows HTTP and FTP URLs to be opened with Kernel.open and read as if they were files:

# Read files concurrently. Use with the "open-uri" module to fetch URLs.
# Pass an array of filenames. Returns a hash mapping filenames to content.
def conread(filenames)
  h = {}                            # Empty hash of results

  # Create one thread for each file
  filenames.each do |filename|      # For each named file
    h[filename] = Thread.new do     # Create a thread, map to filename
      open(filename) {|f| f.read }  # Open and read the file
    end                             # Thread value is file contents
  end

  # Iterate through the hash, waiting for each thread to complete.
  # Replace the thread in the hash with its value (the file contents)
  h.each_pair do |filename, thread| 
    begin
      h[filename] = thread.value    # Map filename to file contents
    rescue
      h[filename] = $!              # Or to the exception raised
    end
  end
end

A Multithreaded Server

Another, almost canonical, use case for threads is for writing servers that can communicate with more than one client at a time. We saw how to do this using multiplexing with Kernel.select, but a somewhat simpler (though possibly less scalable) solution uses threads:

require 'socket'

# This method expects a socket connected to a client.
# It reads lines from the client, reverses them and sends them back.
# Multiple threads may run this method at the same time.
def handle_client(c)
  while true
    input = c.gets.chop     # Read a line of input from the client
    break if !input         # Exit if no more input
    break if input=="quit"  # or if the client asks to.
    c.puts(input.reverse)   # Otherwise, respond to client.
    c.flush                 # Force our output out
  end
  c.close                   # Close the client socket
end


server = TCPServer.open(2000) # Listen on port 2000

while true                    # Servers loop forever
  client = server.accept      # Wait for a client to connect
  Thread.start(client) do |c| # Start a new thread 
    handle_client(c)          # And handle the client on that thread
  end
end

Concurrent iterators

Although IO-bound tasks are the typical use case for Ruby’s threads, they are not restricted to that use. The following code adds a method conmap (for concurrent map) to the Enumerable module. It works like map but processes each element of the input array using a separate thread:

module Enumerable           # Open the Enumerable module
  def conmap(&block)        # Define a new method that expects a block
    threads = []            # Start with an empty array of threads
    self.each do |item|     # For each enumerable item
      # Invoke the block in a new thread, and remember the thread
      threads << Thread.new { block.call(item) }
    end
    # Now map the array of threads to their values 
    threads.map {|t| t.value } # And return the array of values
  end
end

And here’s a similar concurrent version of the each iterator:

module Enumerable
  def concurrently
    map {|item| Thread.new { yield item }}.each {|t| t.join }
  end
end

The code is succinct and challenging: if you can make sense of it, you are well on your way to mastery of Ruby syntax and Ruby iterators.

Recall that in Ruby 1.9, standard iterators that are not passed a block return an enumerator object. This means that given the concurrently method defined earlier and a Hash object h, we can write:

h.each_pair.concurrently {|*pair| process(pair)}

Thread Exclusion and Deadlock

If two threads share access to the same data, and at least one of the threads modifies that data, you must take special care to ensure that no thread can ever see the data in an inconsistent state. This is called thread exclusion. A couple of examples will explain why it is necessary.

First, suppose that two threads are processing files and each thread increments a shared variable in order to keep track of the total number of files processed. The problem is that incrementing a variable is not an atomic operation. That means that it does not happen in a single step: to increment a variable, a Ruby program must read its value, add 1, and then store the new value back into the variable. Suppose that our counter is at 100, and imagine the following interleaved execution of the two threads. The first thread reads the value 100, but before it can add 1, the scheduler stops running the first thread and allows the second thread to run. Now the second thread reads the value 100, adds 1, and stores 101 back into the counter variable. This second thread now starts to read a new file, which causes it to block and allows the first thread to resume. The first thread now adds 1 to 100 and stores the result. Both threads have incremented the counter, but its value is 101 instead of 102.

Another classic example of the need for thread exclusion involves an electronic banking application. Suppose one thread is processing a transfer of money from a savings account to a checking account, and another thread is generating monthly reports to be sent out to customers. Without proper exclusion, the report-generation thread might read the customers’ account data after funds had been subtracted from savings but before they had been added to checking.

We resolve problems like these by using a cooperative locking mechanism. Each thread that wants to access shared data must first lock that data. The lock is represented by a Mutex (short for “mutual exclusion”) object. To lock a Mutex, you call its lock method. When you’re done reading or altering the shared data, you call the unlock method of the Mutex. The lock method blocks when called on a Mutex that’s already locked, and it does not return until the caller has successfully obtained a lock. If each thread that accesses the shared data locks and unlocks the Mutex correctly, no thread will see the data in an inconsistent state and we won’t have problems like those we’ve described.

Mutex is a core class in Ruby 1.9 and is part of the standard thread library in Ruby 1.8. Instead of using the lock and unlock methods explicitly, it is more common to use the synchronize method and associate a block with it. synchronize locks the Mutex, runs the code in the block, and then unlocks the Mutex in an ensure clause so that exceptions are properly handled. Here is a simple model of our bank account example, using a Mutex object to synchronize thread access to shared account data:

require 'thread'  # For Mutex class in Ruby 1.8

# A BankAccount has a name, a checking amount, and a savings amount.
class BankAccount
  def init(name, checking, savings)
    @name,@checking,@savings = name,checking,savings 
    @lock = Mutex.new         # For thread safety
  end

  # Lock account and transfer money from savings to checking
  def transfer_from_savings(x)
    @lock.synchronize {
      @savings -= x
      @checking += x
    }
  end

  # Lock account and report current balances
  def report
    @lock.synchronize {
      "#@name
Checking: #@checking
Savings: #@savings"
    }
  end
end

Deadlock

When we start using Mutex objects for thread exclusion we must be careful to avoid deadlock. Deadlock is the condition that occurs when all threads are waiting to acquire a resource held by another thread. Because all threads are blocked, they cannot release the locks they hold. And because they cannot release the locks, no other thread can acquire those locks.

A classic deadlock scenario involves two threads and two Mutex objects. Thread 1 locks Mutex 1 and then attempts to lock Mutex 2. Meanwhile, thread 2 locks Mutex 2 and then attempts to lock Mutex 1. Neither thread can acquire the lock it needs, and neither thread can release the lock the other one needs, so both threads block forever:

# Classic deadlock: two threads and two locks
require 'thread'

m,n = Mutex.new, Mutex.new

t = Thread.new {
  m.lock
  puts "Thread t locked Mutex m"
  sleep 1
  puts "Thread t waiting to lock Mutex n"
  n.lock
}

s = Thread.new {
  n.lock
  puts "Thread s locked Mutex n"
  sleep 1
  puts "Thread s waiting to lock Mutex m"
  m.lock
}

t.join
s.join

The way to avoid this kind of deadlock is to always lock resources in the same order. If the second thread locked m before locking n, then deadlock would not occur.

Note that deadlock is possible even without using Mutex objects. Calling join on a thread that calls Thread.stop will deadlock both threads, unless there is a third thread that can awaken the stopped thread.

Bear in mind that some Ruby implementations can detect simple deadlocks like these and abort with an error, but this is not guaranteed.

Queue and SizedQueue

The standard thread library defines the Queue and SizedQueue data structures specifically for concurrent programming. They implement thread-safe FIFO queues and are intended for a producer/consumer model of programming. Under this model, one thread produces values of some sort and places them on a queue with the enq (enqueue) method or its synonym push. Another thread “consumes” these values, removing them from the queue with the deq (dequeue) method as needed. (The pop and shift methods are synonyms for deq.)

The key features of Queue that make it suitable for concurrent programming is that the deq method blocks if the queue is empty and waits until the producer thread adds a value to the queue. The Queue and SizedQueue classes implement the same basic API, but the SizedQueue variant has a maximum size. If the queue is already at its maximum size, then the method for adding a value to the queue will block until the consumer thread removes a value from the queue.

As with Ruby’s other collection classes, you can determine the number of elements in a queue with size or length, and you can determine if a queue is empty with empty?. Specify the maximum size of a SizedQueue when you call SizedQueue.new. After creating a SizedQueue, you can query and alter its maximum size with max and max=.

Earlier in this chapter, we saw how to add a concurrent map method to the Enumerable module. We now define a method that combines a concurrent map with a concurrent inject. It creates a thread for each element of the enumerable collection and uses that thread to apply a mapping Proc. The value returned by that Proc is enqueued on a Queue object. One final thread acts as a consumer; it removes values from the queue and passes them to the injection Proc as they become available.

We call this concurrent injection method conject, and you could use it like this to concurrently compute the sum of the squares of the values in an array. Note that a sequential algorithm would almost certainly be faster for a simple sum-of-squares example like this:

a = [-2,-1,0,1,2]
mapper = lambda {|x| x*x }             # Compute squares
injector = lambda {|total,x| total+x } # Compute sum
a.conject(0, mapper, injector)         # => 10

The code for this conject method is as follows—note the use of a Queue object and its enq and deq methods:

module Enumerable
  # Concurrent inject: expects an initial value and two Procs
  def conject(initial, mapper, injector)
    # Use a Queue to pass values from mapping threads to injector thread
    q = Queue.new   
    count = 0                 # How many items?
    each do |item|            # For each item
      Thread.new do           # Create a new thread
        q.enq(mapper[item])   # Map and enqueue mapped value
      end
      count += 1              # Count items
    end

    t = Thread.new do         # Create injector thread
      x = initial             # Start with specified initial value
      while(count > 0)        # Loop once for each item
        x = injector[x,q.deq] # Dequeue value and inject
        count -= 1            # Count down
      end
      x                       # Thread value is injected value
    end

    t.value   # Wait for injector thread and return its value
  end
end

Condition Variables and Queues

There is something important to notice about the Queue class: the deq method can block. Normally, we only think of blocking as happening with IO methods (or when calling join on a thread or lock on a Mutex). In multithreaded programming, however, it is sometimes necessary to have a thread wait for some condition (outside of the control of that thread) to become true. In the case of the Queue class, the condition is the nonempty status of the queue: if the queue is empty, then a consumer thread must wait until a producer thread calls enq and makes the queue nonempty.

Making a thread wait until some other thread tells it that it can go again is accomplished most cleanly with a ConditionVariable. Like Queue, ConditionVariable is part of the standard thread library. Create a ConditionVariable with ConditionVariable.new. Make a thread wait on the condition with the wait method. Wake one waiting thread with signal. Wake all waiting threads with broadcast. There is one slightly tricky part to the use of condition variables: in order to make things work correctly, the waiting thread must pass a locked Mutex object to the wait method. This mutex will be temporarily unlocked while the thread waits, and it will be locked again when the thread wakes up.

We conclude our coverage of threads with a utility class that is sometimes useful in multithreaded programs. It is called Exchanger, and it allows two threads to swap arbitrary values. Suppose we have threads t1 and t2 and an Exchanger object e. t1 calls e.exchange(1). This method then blocks (using a ConditionVariable, of course) until t2 calls e.exchange(2). This second thread does not block, it simply returns 1—the value passed by t1. Now that the second thread has called exchange, t1 wakes up again and returns 2 from the exchange method.

The Exchanger implementation shown here is somewhat complex, but it demonstrates a typical use of the ConditionVariable class. One interesting feature of this code is that it uses two Mutex objects. One of them is used to synchronize access to the exchange method and is passed to the wait method of the condition variable. The other Mutex is used to determine whether the calling thread is the first or the second thread to invoke exchange. Instead of using lock with this Mutex, this class uses the nonblocking try_lock method. If @first.try_lock returns true, then the calling thread is the first thread. Otherwise, it is the second thread:

require 'thread'

class Exchanger
  def initialize
    # These variables will hold the two values to be exchanged.
    @first_value = @second_value = nil
    # This Mutex protects access to the exchange method.
    @lock = Mutex.new
    # This Mutex allows us to determine whether we're the first or
    # second thread to call exchange.
    @first = Mutex.new
    # This ConditionVariable allows the first thread to wait for
    # the arrival of the second thread.
    @second = ConditionVariable.new
  end

  # Exchange this value for the value passed by the other thread.
  def exchange(value)
    @lock.synchronize do      # Only one thread can call this method at a time
      if @first.try_lock      # We are the first thread
        @first_value = value  # Store the first thread's argument
        # Now wait until the second thread arrives.
        # This temporarily unlocks the Mutex while we wait, so 
        # that the second thread can call this method, too
        @second.wait(@lock)   # Wait for second thread 
        @first.unlock         # Get ready for the next exchange
        @second_value         # Return the second thread's value
      else                    # Otherwise, we're the second thread
        @second_value = value # Store the second value
        @second.signal        # Tell the first thread we're here
        @first_value          # Return the first thread's value
      end
    end
  end
end
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.164.143