Ruby’s use of blocks, coupled with its parentheses-optional syntax,
make it very easy to define iterator methods that look like and behave
like control structures. The loop
method of Kernel
is a simple example.
In this section we develop three more examples. The examples here use
Ruby’s threading API; you may need to read Threads and Concurrency to
understand all the details.
Example 8-1 defines global methods named
after
and every
. Each takes a numeric argument that
represents a number of seconds and should have a block associated with
it. after
creates a new thread and
returns the Thread
object
immediately. The newly created thread sleeps for the specified number
of seconds and then calls (with no arguments) the block you provided.
every
is similar, but it calls the
block repeatedly, sleeping the specified number of seconds between
calls. The second argument to every
is a value to pass to the first invocation of the block. The return
value of each invocation becomes the value passed for the next
invocation. The block associated with every
can use break
to prevent any future
invocations.
Here is some example code that uses after
and every
:
require 'afterevery' 1.upto(5) {|i| after i { puts i} } # Slowly print the numbers 1 to 5 sleep(5) # Wait five seconds every 1, 6 do |count| # Now slowly print 6 to 10 puts count break if count == 10 count + 1 # The next value of count end sleep(6) # Give the above time to run
The sleep
call at the end of
this code prevents the example program from exiting before the thread
created by every
can complete its
count. With that example of how after
and every
are used, we are now ready to present
their implementation. Remember to consult Threads and Concurrency
if you don’t understand Thread.new
.
Example 8-1. The after and every methods
# # Define Kernel methods after and every for deferring blocks of code. # Examples: # # after 1 { puts "done" } # every 60 { redraw_clock } # # Both methods return Thread objects. Call kill on the returned objects # to cancel the execution of the code. # # Note that this is a very naive implementation. A more robust # implementation would use a single global timer thread for all tasks, # would allow a way to retrieve the value of a deferred block, and would # provide a way to wait for all pending tasks to complete. # # Execute block after sleeping the specified number of seconds. def after(seconds, &block) Thread.new do # In a new thread... sleep(seconds) # First sleep block.call # Then call the block end # Return the Thread object right away end # Repeatedly sleep and then execute the block. # Pass value to the block on the first invocation. # On subsequent invocations, pass the value of the previous invocation. def every(seconds, value=nil, &block) Thread.new do # In a new thread... loop do # Loop forever (or until break in block) sleep(seconds) # Sleep value = block.call(value) # And invoke block end # Then repeat.. end # every returns the Thread end
When writing programs that use multiple threads, it is important that two
threads do not attempt to modify the same object at the same time. One
way to do this is to place the code that must be made thread-safe in a
block associated with a call to the synchronize
method of a Mutex
object. Again, this is discussed in detail in Threads and Concurrency. In Example 8-2
we take this a step further, and emulate Java’s synchronized
keyword with a global method
named synchronized
. This synchronized
method expects a single object
argument and a block. It obtains a Mutex
associated with the object, and uses
Mutex.synchronize
to invoke the block.
The tricky part is that Ruby’s object, unlike Java’s objects, do not
have a Mutex
associated with them.
So Example 8-2 also defines an instance method
named mutex
in Object
. Interestingly, the implementation of
this mutex
method uses synchronized
in its new keyword-style
form!
Example 8-2. Simple synchronized blocks
require 'thread' # Ruby 1.8 keeps Mutex in this library # Obtain the Mutex associated with the object o, and then evaluate # the block under the protection of that Mutex. # This works like the synchronized keyword of Java. def synchronized(o) o.mutex.synchronize { yield } end # Object.mutex does not actually exist. We've got to define it. # This method returns a unique Mutex for every object, and # always returns the same Mutex for any particular object. # It creates Mutexes lazily, which requires synchronization for # thread safety. class Object # Return the Mutex for this object, creating it if necessary. # The tricky part is making sure that two threads don't call # this at the same time and end up creating two different mutexes. def mutex # If this object already has a mutex, just return it return @__mutex if @__mutex # Otherwise, we've got to create a mutex for the object. # To do this safely we've got to synchronize on our class object. synchronized(self.class) { # Check again: by the time we enter this synchronized block, # some other thread might have already created the mutex. @__mutex = @__mutex || Mutex.new } # The return value is @__mutex end end # The Object.mutex method defined above needs to lock the class # if the object doesn't have a Mutex yet. If the class doesn't have # its own Mutex yet, then the class of the class (the Class object) # will be locked. In order to prevent infinite recursion, we must # ensure that the Class object has a mutex. Class.instance_eval { @__mutex = Mutex.new }
3.148.108.112