You need to protect a piece of data, like a counter or an output stream, from getting garbled by multiple threads.
Three choices, hot shot:
- Explicit locks (aka mutexes): acquire a lock around the “critical section”, munge the data, release the lock. You have to manage the lock yourself. Multiple threads accessing the lock will not run concurrently anymore.
- Implicit locks (aka monitors): annotate methods that modify important data. The monitor library manages the lock for you. Threads still serialize around the lock, reducing concurrency.
- Atomic objects (aka compare-and-swap): use data structures that take advantage of runtime or processor semantics to guarantee that competing threads never interfere with each other. No locks! Much less serializing! Not broadly applicable, but I highly recommend them when you have the means.
Mutexes, aka lock “classic”
Mutexes are the lowest level of locks, at least in Ruby. They are the ur-locks, the most primitive of locks; everything is built on top of them. With any luck, you won’t ever need to use them directly, but it helps knowing how they work.
Eighty percent of what you need to know is synchronize
. You create a lock, and then you use it to protect a piece of code that would go sideways if multiple threads hit it at the exact same time. Here’s a little class that locks around printing to standard output:
class Output
def initialize
@lock = Mutex.new
end
def log(msg)
@lock.synchronize { puts msg }
end
end
Using Output#log
instead of puts
will prevent the output of your multithreaded program from getting jumbled and confused by everyone writing to stdout
at the same time. You could manually lock
and unlock
a Mutex if you had special needs.
Let’s talk counters
For the next couple examples, we’re going to implement a counter. Multiple threads will update said counter, so it needs to protect itself. Here’s how we use the counter:
require 'thread'
CORES=2
ITERS=1_000
threads = CORES.times.map do |n|
Thread.new do
ITERS.times do |i|
out.log("Thread #{n}: Iteration: #{i} Counter: #{counter.value}") if i % 100 == 0
counter.incr
end
end
end
threads.each(&:join)
p counter.value
My Macbook Air has two real cores (don’t believe the hype!) and we’ll increment the counter a thousand times in each thread. Every hundred times through the loop, we’ll show some progress. At the end, we join
each thread and then print the value of our counter. If all goes well, it will be CORES * ITERS
.
All would not go well with this naive implementation:
class WildCounter
def initialize
@counter = 0
end
def incr
@counter = @counter + 1
end
def value
@counter
end
end
If two threads execute incr
at the same time, they will misread @counter
or unintentionally overwrite a perfectly good value that was incremented behind their back.
We could protect this counter with a mutex, but I want to show you two other ways to go about it.
Monitors, aka intrinsic locks
Turns out, a well-designed class will tend to isolate state changes to a few methods. These “tell, don’t ask” methods are what you’ll likely end up locking. It would be pretty rad if you could just wrap a lock around the whole method without having to create variables and do a bunch of typing, don’t you think?
Those are a thing! They’re called monitors. You can read a bunch of academic stuff about them, but the crux of the biscuit is, a monitor is a lock around an entire instance of an object. You then declare methods that can only execute when that lock is held. Here’s a counter that uses a monitor:
require 'monitor'
class MonitorCounter
def initialize
@counter = 0
# No idea why this doesn't work inside the class declaration
extend(MonitorMixin)
end
def incr
synchronize { @counter = @counter + 1 }
end
def value
@counter
end
end
It doesn’t look too much different from our naive counter. In the constructor, we extend Ruby’s MonitorMixin
, which imbues this class with a lock and a synchronize
method to protect mutator methods. (Ed. if anyone knows why the extend
has to happen in the constructor instead of in the class declaration, I’m extremely stumped as to why!)
In incr
, where we do the dirty work of updating the counter, all we need to do is put the actual logic inside a synchronize
block. This ensures that only thread may execute this method on any given object instance at a time. Two threads could increment two counters safely, but if those two threads want to increment the same counter, they have to take turns.
A brief note on terminology: many Java concurrency texts refer to monitors as “intrinsic” locks because, in Java, they are part of every object. Mutexes are referred to as “extrinsic” locks because they aren’t tightly associated with any particular object instance.
Atomics, aka “wow that’s clever!”
It turns out that, in some cases, you can skip locks altogether. Amazing, right?!
Unfortunately, Ruby doesn’t have core support for atomic objects. Fortunately, Charles Nutter’s atomic
library provides just that. It exploits operations provided by the underlying platform (the JVM in the case of JRuby, atomic compare-and-swap operations on Intel in the case of Rubinius) to implement objects that are guaranteed to update within one processor clock cycle. These operations work by taking two parameters, the old value and the new value; if the current value matches the old value, it’s safe to update it to the new value. If it doesn’t match, the operation fails and you have to try again.
Phew! Now you know a lot about atomic processor operations.
“Show me right now, Adam!” you say. Much obliged.
require 'atomic'
class AtomicCounter
def initialize
@counter = ::Atomic.new(0)
end
def incr
@counter.update { |v| v + 1 }
end
def value
@counter.value
end
end
Luckily, Atomic
encapsulates all the business of comparing and swapping and knowing about how to use atomic instructions. It maintains the value of the object internally and handles all the swapping logic for you. Call update
, change the object in the block, and go on with your life. No locks necessary!
If that doesn’t make you love modern computer hardware, you are a programmer who does not know joy.
Tread carefully
Congratulations, you are now somewhat conversant on the topic of locking in concurrent Ruby programs. You know what the tools are, but, unfortunately, I haven’t the space to educate you on all the ways you are now equipped to shoot yourself in the foot. If you’re curious, you can read up on deadlock, livelock, starvation, priority inversion, and all the failure cases for dead processes left holding a lock.
The principle I try to follow, when I’m presented with a problem that needs locking, is to ask if I can work around the need for locking somehow. Could I use a Queue or atomic? Could I isolate this state in one thread and obviate the need for the lock? Is this state really necessary at all?
To anti-quote Ferris Buehler’s Day Off, when it comes to adding locks, “I highly unrecommend it, if you have the means”.