This section introduces threads, which are Ruby’s control structure for concurrent execution, and also two more esoteric control structures, called fibers and continuations.
A thread of execution is a sequence of Ruby statements that run (or appear to
run) in parallel with the main sequence of statements that the
interpreter is running. Threads are represented by Thread
objects, but they can also be thought
of as control structures for concurrency. Concurrent programming in
Ruby is covered in detail in Threads and Concurrency. This section
is just a simple overview that shows how to create threads.
Ruby’s use of blocks makes it very easy to create new threads.
Simply call Thread.new
and associate a block with it. A new thread of execution
will be created and will start running the code in the block.
Meanwhile, the original thread will return from the Thread.new
call and will continue with the
following statement. The newly created thread will exit when the block
exits. The return value of the block becomes available through the
value
method of the Thread
object. (If you call this method
before the thread has completed, the caller will block until the
thread returns a value.)
The following code shows how you might use threads to read the contents of multiple files in parallel:
# This method expects an array of filenames. # It returns an array of strings holding the content of the named files. # The method creates one thread for each named file. def readfiles(filenames) # Create an array of threads from the array of filenames. # Each thread starts reading a file. threads = filenames.map do |f| Thread.new { File.read(f) } end # Now create an array of file contents by calling the value # method of each thread. This method blocks, if necessary, # until the thread exits with a value. threads.map {|t| t.value } end
See Threads and Concurrency for much more about threads and concurrency in Ruby.
Ruby 1.9 introduces a control structure known as a
fiber and represented by an object of class
Fiber
. The name “fiber” has been
used elsewhere for a kind of lightweight thread, but Ruby’s fibers are
better described as coroutines or, more accurately, semicoroutines.
The most common use for coroutines is to implement
generators: objects that can compute a partial
result, yield the result back to the caller, and save the state of the
computation so that the caller can resume that computation to obtain
the next result. In Ruby, the Fiber
class is used to enable the automatic conversion of internal
iterators, such as the each
method,
into enumerators or external iterators.
Note that fibers are an advanced and relatively obscure control
structure; the majority of Ruby programmers will never need to use the
Fiber
class directly. If you have
never programed with coroutines or generators before, you may find
them difficult to understand at
first. If so, study the examples carefully and try out some examples
of your own.
A fiber has a body of code like a thread does. Create a fiber
with Fiber.new
, and associate a block with it to specify the code that
the fiber is to run. Unlike a thread, the body of a fiber does not
start executing right away. To run a fiber, call the resume
method of the Fiber
object that represents it. The first
time resume
is called on a fiber,
control is transferred to the beginning of the fiber body. That fiber
then runs until it reaches the end of the body, or until it executes
the class method Fiber.yield
. The
Fiber.yield
method transfers
control back to the caller and makes the call to resume
return. It also saves the state of
the fiber, so that the next call to resume
makes the fiber pick up where it left
off. Here is a simple example:
f = Fiber.new { # Line 1: Create a new fiber puts "Fiber says Hello" # Line 2: Fiber.yield # Line 3: goto line 9 puts "Fiber says Goodbye" # Line 4: } # Line 5: goto line 11 # Line 6: puts "Caller says Hello" # Line 7: f.resume # Line 8: goto line 2 puts "Caller says Goodbye" # Line 9: f.resume # Line 10: goto line 4 # Line 11:
The body of the fiber does not run when it is first created, so
this code creates a fiber but does not produce any output until it
reaches line 7. The resume
and
Fiber.yield
calls then transfer
control back and forth so that the messages from the fiber and the
caller are interleaved. The code produces the following output:
Caller says Hello Fiber says Hello Caller says Goodbye Fiber says Goodbye
It is worth noting here that the “yielding” performed by
Fiber.yield
is completely different
than the yielding performed by the yield
statement. Fiber.yield
yields control from the current
fiber back to the caller that invoked it. The yield
statement, on the other hand, yields
control from an iterator method to the block associated with the
method.
Fibers and their callers can exchange data through the arguments and return
values of resume
and yield
. The arguments to the first call to
resume
are passed to the block
associated with the fiber: they become the values of the block
parameters. On subsequent
calls, the arguments to resume
become the return value of Fiber.yield
. Conversely, any arguments to Fiber.yield
become the return value of
resume
. And when the block exits,
the value of the last expression evaluated also becomes the return
value of resume
. The following
code demonstrates this:
f = Fiber.new do |message| puts "Caller said: #{message}" message2 = Fiber.yield("Hello") # "Hello" returned by first resume puts "Caller said: #{message2}" "Fine" # "Fine" returned by second resume end response = f.resume("Hello") # "Hello" passed to block puts "Fiber said: #{response}" response2 = f.resume("How are you?") # "How are you?" returned by Fiber.yield puts "Fiber said: #{response2}"
The caller passes two messages to the fiber, and the fiber returns two responses to the caller. It prints:
Caller said: Hello Fiber said: Hello Caller said: How are you? Fiber said: Fine
In the caller’s code, the messages are always arguments to
resume
, and the responses are
always the return value of that method. In the body of the fiber,
all messages but the first are received as the return value of
Fiber.yield
, and all responses
but the last are passed as arguments to Fiber.yield
. The first message is received
through block parameters, and the last response is the return value
of the block itself.
The fiber examples shown so far have not been terribly
realistic. Here we demonstrate some more typical uses. First, we
write a Fibonacci number generator—a Fiber
object that returns successive
members of the Fibonacci sequence on each call to resume
:
# Return a Fiber to compute Fibonacci numbers def fibonacci_generator(x0,y0) # Base the sequence on x0,y0 Fiber.new do x,y = x0, y0 # Initialize x and y loop do # This fiber runs forever Fiber.yield y # Yield the next number in the sequence x,y = y,x+y # Update x and y end end end g = fibonacci_generator(0,1) # Create a generator 10.times { print g.resume, " " } # And use it
The code above prints the first 10 Fibonacci numbers:
1 1 2 3 5 8 13 21 34 55
Because Fiber
is a
confusing control structure, we might prefer to hide its API when
writing generators. Here is another version of a Fibonacci number
generator. It defines its own class and implements the same next
and rewind
API that enumerators do:
class FibonacciGenerator def initialize @x,@y = 0,1 @fiber = Fiber.new do loop do @x,@y = @y, @x+@y Fiber.yield @x end end end def next # Return the next Fibonacci number @fiber.resume end def rewind # Restart the sequence @x,@y = 0,1 end end g = FibonacciGenerator.new # Create a generator 10.times { print g.next, " " } # Print first 10 numbers g.rewind; puts # Start over, on a new line 10.times { print g.next, " " } # Print the first 10 again
Note that we can make this FibonacciGenerator
class Enumerable
by including the Enumerable
module and adding the following
each
method (which we first used
in External Iterators):
def each loop { yield self.next } end
Conversely, suppose we have an Enumerable
object and want to make an
enumerator-style generator out of it. We can use this class:
class Generator def initialize(enumerable) @enumerable = enumerable # Remember the enumerable object create_fiber # Create a fiber to enumerate it end def next # Return the next element @fiber.resume # by resuming the fiber end def rewind # Start the enumeration over create_fiber # by creating a new fiber end private def create_fiber # Create the fiber that does the enumeration @fiber = Fiber.new do # Create a new fiber @enumerable.each do |x| # Use the each method Fiber.yield(x) # But pause during enumeration to return values end raise StopIteration # Raise this when we're out of values end end end g = Generator.new(1..10) # Create a generator from an Enumerable like this loop { print g.next } # And use it like an enumerator like this g.rewind # Start over like this g = (1..10).to_enum # The to_enum method does the same thing loop { print g.next }
Although it is useful to study the implementation of this
Generator
class, the class itself
doesn’t provide any functionality over that provided by the to_enum
method.
The fiber
module in the standard library enables additional, more
powerful features of the fibers. To use these features, you
must:
require 'fiber'
However, you should avoid using these additional features wherever possible, because:
They are not supported by all implementations. JRuby, for example, cannot support them on current Java VMs.
They are so powerful that misusing them can crash the Ruby VM.
The core features of the Fiber
class implement semicoroutines.
These are not true coroutines because there is a fundamental
asymmetry between the caller and the fiber: the caller uses resume
and the fiber uses yield
. If you require the fiber
library, however, the Fiber
class gets a transfer
method that allows any fiber to
transfer control to any other fiber. Here is an example in which two
fibers use the transfer
method to
pass control (and values) back and forth:
require 'fiber' f = g = nil f = Fiber.new {|x| # 1: puts "f1: #{x}" # 2: print "f1: 1" x = g.transfer(x+1) # 3: pass 2 to line 8 puts "f2: #{x}" # 4: print "f2: 3" x = g.transfer(x+1) # 5: return 4 to line 10 puts "f3: #{x}" # 6: print "f3: 5" x + 1 # 7: return 6 to line 13 } g = Fiber.new {|x| # 8: puts "g1: #{x}" # 9: print "g1: 2" x = f.transfer(x+1) #10: return 3 to line 3 puts "g2: #{x}" #11: print "g2: 4" x = f.transfer(x+1) #12: return 5 to line 5 } puts f.transfer(1) #13: pass 1 to line 1
This code produces the following output:
f1: 1 g1: 2 f2: 3 g2: 4 f3: 5 6
You will probably never need to use this transfer
method, but its existence helps
explain the name “fiber.” Fibers can be thought of as independent
paths of execution within a single thread of execution. Unlike
threads, however, there is no scheduler to transfer control among
fibers; fibers must explicitly schedule themselves with transfer
.
In addition to the transfer
method, the fiber
library also
defines an instance method alive?
, to determine if the body of a
fiber is still running, and a class method current
, to return the Fiber
object that currently has
control.
A continuation is another complex and obscure control
structure that most programmers will never need to use. A continuation
takes the form of the Kernel
method
callcc
and the Continuation
object. Continuations are part
of the core platform in Ruby 1.8, but they have been replaced by
fibers and moved to the standard library in Ruby 1.9. To use them in
Ruby 1.9, you must explicitly require them with:
require 'continuation'
Implementation difficulties prevent other implementations of Ruby (such as JRuby, the Java-based implementation) from supporting continuations. Because they are no longer well supported, continuations should be considered a curiosity, and new Ruby code should not use them. If you have Ruby 1.8 code that relies on continuations, you may be able to convert it to use fibers in Ruby 1.9.
The Kernel
method callcc
executes its block, passing a newly
created Continuation
object as the
only argument. The Continuation
object has a call
method, which
makes the callcc
invocation return
to its caller. The value passed to call
becomes the return value of the
callcc
invocation. In this sense,
callcc
is like catch
, and the call
method of
the Continuation
object is like
throw
.
Continuations are different, however, because the Continuation
object can be saved into a
variable outside of the callcc
block. The call
method of this
object may be called repeatedly, and causes control to jump to the
first statement following the callcc
invocation.
The following code demonstrates how continuations can be used to
define a method that works like the goto
statement in the BASIC programming
language:
# Global hash for mapping line numbers (or symbols) to continuations $lines = {} # Create a continuation and map it to the specified line number def line(symbol) callcc {|c| $lines[symbol] = c } end # Look up the continuation associated with the number, and jump there def goto(symbol) $lines[symbol].call end # Now we can pretend we're programming in BASIC i = 0 line 10 # Declare this spot to be line 10 puts i += 1 goto 10 if i < 5 # Jump back to line 10 if the condition is met line 20 # Declare this spot to be line 20 puts i -= 1 goto 20 if i > 0
52.15.42.128