Developer at large, expert typist, fungineer

Protect that state: locks, monitors, and atomics

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”.

3 Responses to “Protect that state: locks, monitors, and atomics”

  1. Adam Stankiewicz (@sheerun)

    because ‘include’ in class body is the same as ‘extend’ in it’s initializer (or extend method on it’s instance). as described in documentation, you include mixin. great post btw :)

  2. Matthew Boeh

    Hi Adam,

    This is a great post. I linked to it at http://ruby.io, a site that some of us are putting together to collect resources and documentation on concurrency, parallelism, and performance in Ruby. Hope you don’t mind.

  3. Adam Keys

    That’s strange; I tried every permutation of include/extend on MonitorMixin and the way I have it listed is the only way that works. Clearly, I subtly suck at Ruby!

Comments are closed.

Follow

Get every new post delivered to your Inbox.

Join 2,590 other followers

%d bloggers like this: