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