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
.
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 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
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
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.
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 }}
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)}
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.
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.
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.
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.
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.
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.
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.
Now that we’ve explained Ruby’s thread model and thread API, we’ll take a look at some actual examples of multithreaded code.
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
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
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)}
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
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.
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
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
18.217.164.143