In Progress
Unit 1, Lesson 21
In Progress

Monitor

Video transcript & code

So let's say we've been contracted by the international turtle-racing association to build the next generation of real-time tracking software for turtle races. As part of the job, we need to track the current score of every turtle in a race on a moment-by-moment basis. The score is measured in terms of the number of inches that turtle has traveled.

We've already constructed a state-of-the-art turtle-recognition system which pinpoints the location of each turtle on the racing field. Because turtle races move fast, each turtle is tracked by an independent thread of execution. As a result, our score-tracking class must be thread-safe.

We've taken the lessons of previous episodes on thread-safety to heart, and we know we can't use a plain Ruby Hash to track scores and depend on it to be thread-safe on all platforms. So we build ourselves a class that gives us a thread-safe interface to just the operations we need for tracking scores.

We give this class a scores hash and a mutex to be used for synchronization. We define an #update method for entering the latest score for a given turtle. It uses the Mutex#synchronize method to ensure that only one thread will be updating the score at a time.

We also give it a #for_turtle method which returns the current score for a given turtle.

This all works quite well. We can update turtle scores and retrieve them by name.

class Scores
  def initialize
    @scores = {}
    @lock   = Mutex.new
  end

  def update(turtle, score)
    @lock.synchronize do
      @scores[turtle] = score
    end
  end

  def for_turtle(turtle)
    @lock.synchronize do
      @scores[turtle]
    end
  end
end

scores = Scores.new
scores.update("yertle", 23)
scores.update("mack", 15)
scores.for_turtle("mack")              # => 15

Later on, as we're developing the user interface for the software, we discover that many users want to track their favorite turtles in real time. To facilitate this, we add a new method #for_all_turtles, which takes a list of turtle names and returns scores for each of them in order. Again, we wrap the body of the method in a synchronization block. This block is particular important in this case, since we want to produce an accurate snapshot of the race status at a given point in time. It wouldn't do to have one of the scores updated while we were in the middle of collecting them for return.

Inside this block, we decide to re-use our #for_turtle method from earlier to retrieve the individual scores.

Unfortunately, when we try this out we run into a problem.

require "thread"

class Scores
  def initialize
    @scores = {}
    @lock   = Mutex.new
  end

  def update(turtle, score)
    @lock.synchronize do
      @scores[turtle] = score
    end
  end

  def for_turtle(turtle)
    @lock.synchronize do
      @scores[turtle]
    end
  end

  def for_all(*turtles)
    @lock.synchronize do
      turtles.map do |p|
        for_turtle(p)
      end
    end
  end
end


scores = Scores.new
scores.update("yertle", 23)
scores.update("mack", 15)
scores.for_turtle("mack")              # => 15
scores.for_all("mack", "yertle") # => 
# ~> <internal:prelude>:8:in `lock': deadlock; recursive locking (ThreadError)
# ~>    from <internal:prelude>:8:in `synchronize'
# ~>    from -:16:in `for_turtle'
# ~>    from -:24:in `block (2 levels) in for_all'
# ~>    from -:23:in `map'
# ~>    from -:23:in `block in for_all'
# ~>    from <internal:prelude>:10:in `synchronize'
# ~>    from -:22:in `for_all'
# ~>    from -:35:in `<main>'

Ruby tells us that we have a deadlock on our hands. Specifically, a "recursive locking" deadlock.

What does this mean? The answer is simple: Ruby's mutexes are not recursive, meaning that a thread cannot acquire the same mutex twice. The #for_all method acquires a lock and then calls the #for_turtle method which also acquires the same lock. This is a recursive lock, and it's not permitted.

This may seem like an arbitrary restriction, but it exists for a reason. Recursion-aware mutexes require more code and memory than a basic mutex. Consider what would happen if Ruby didn't detect and prevent the recursion here: after the first call to #for_turtle the mutex would be released, and other threads would be free to update scores in the middle of the executoin of #for_all—even though that's precisely what we set out to prevent by surrounding this method in a #synchronize block.

Some languages provide recursive mutex types in addition to the basic nonrecursive kind, but Ruby is not one of those languages. Actually, that's not completely accurate. While Ruby does not give us anything called a recursive mutex, it makes available a module with equivalent functionality.

To get at this module, we require the monitor library. We then get rid of our mutex, and instead include MonitorMixin in our class. We add a call to super in the #initialize method, to ensure that the MonitorMixin is properly initialized. Then in each case where we previously sent the synchronize message to the @lock mutex, we change the code to call send the message to self instead. The MonitorMixin provides this new method to our class.

require "monitor"

class Scores
  include MonitorMixin

  def initialize
    super
    @scores = {}
  end

  def update(turtle, score)
    synchronize do
      @scores[turtle] = score
    end
  end

  def for_turtle(turtle)
    synchronize do
      @scores[turtle]
    end
  end

  def for_all(*turtles)
    synchronize do
      turtles.map do |p|
        for_turtle(p)
      end
    end
  end
end

scores = Scores.new
scores.update("yertle", 23)
scores.update("mack", 15)
scores.for_turtle("mack")              # => 15
scores.for_all("yertle", "mack")       # => [23, 15]

When we run this code, it works perfectly. MonitorMixin implicitly gives our class a built-in mutex, but it also layers more functionality on top of that mutex. Specifically, it adds a recursion counter which is incremented whenever the code enters a synchronization block, and decremented when the code leaves that block. The mutex is only released when the counter reaches zero.

def for_turtle(turtle)
  synchronize do # +1
    @scores[turtle]
  end # -1
end

def for_all(*turtles)
  synchronize do # +1
    turtles.map do |p|
      for_turtle(p)
    end # -1
  end
end

In talking about threads and thread-safety, we covered mutexes before MonitorMixin. We did this in order to start with the fundamentals and then build up from there. Now that we know about both, when building classes that may be shared between threads it's probably simpler and easier to stick to MonitorMixin except when we have a specific reason to use a basic Mutex.

That's all for today. Happy hacking!

Responses