Ruby is often said to not be an appropriate language for concurrent applications, and this criticism tends to come as a result of the Global Interpreter Lock (GIL) which restricts both MRI (Ruby 1.8.x) and YARV (Ruby 1.9+) to running a single thread at a time, despite the fact that YARV maps Ruby threads to real system threads.
It is true that Ruby’s primary implementation cannot, then, do real parallelism. JRuby, on the other hand, does concurrency just fine, since JRuby threads are backed by Java threads, which are in turn scheduled by the operating system. Additionally, there is a difference between concurrency and parallelism. Ruby has many built in features that make concurrency easy to achieve and manage safely.
One issue with concurrent applications is signalling progress among multiple threads of execution. A simple solution can be borrowed from Java’s
CountDownLatch. An object of this type can be used to synchronise threads in time. A
CountDownLatch instance is created with a count, and then a method is called on it to decrement that count. Meanwhile, another thread may call a method on it which blocks until the count reaches zero, at which point the
CountDownLatch is released and the waiting threads may continue execution.
If this sounds somewhat similar to Ruby’s
ConditionVariable, that’s because it is. A
ConditionVariable, however, is essentially a
CountDownLatch with a constant count of one. If we were to need to count down more than one action in Ruby, we’d need to implement an API similar to
CountDownLatch. And so that’s what I’ve done. A simple usage example given in the README goes something like this:
require 'countdownlatch' latch = CountDownLatch.new 2 Thread.new do 2.times do sleep 1 latch.countdown! end end latch.wait 10
So, what’s happening here? Well, we’re first creating a latch which expects to be counted down twice before it releases. Then, we spin up a new thread which sleeps for one second, then counts down the latch; it does this twice, in fact. While this is executing, we block the main thread by calling
#wait on the latch. Additionally, we pass an optional parameter of 10, which is the number of seconds after which we should give up waiting. In this case, the latch will be released after 2 seconds or so, and so you’ll see the call to
true after that time.
So, some use cases:
require 'countdownlatch' latch = CountDownLatch.new 2 2.times do Thread.new do ...some long running process... latch.countdown! end end latch.wait
This is useful if your threads get to a point that you want to be notified of, part of the way through a long execution time. If you only care about when they finish, you could do
threads.each &:join, but this is slightly inflexible. With
CountDownLatch you could easily create a thread pool to which threads could be added and removed, and then await the entire pool finishing. This allows significant fluidity, and is something I’ll demonstrate in a future post.
require 'countdownlatch' latch = CountDownLatch.new 1 10.times do Thread.new do latch.wait ...do something useful, but not before the rest do so also... end end latch.countdown!
Here, each thread’s first operation is to wait on the latch, and once all threads had been created, you release the latch, allowing them to begin operation.
There are many more complex situations where a
CountDownLatch would be useful, and these are highly application specific. It’s true that many of the use cases for
CountDownLatch are the same as those for
ConditionVariable and you could just use the latter, however there are three things that favour the use of
* Consistency: If you have two things which do the same thing, but one is more flexible, why not use the more flexible one all the time, as long as it’s not more complicated?
* Encapsulation: A
CountDownLatch keeps its own Mutex which it uses in combination with the
ConditionVariable, so you don’t have to.
* Identity: A
CountDownLatch can tell you if it’s been released or not. A
* Determinism: If the latch has previously been released, waiting for it will have no effect and return immediately. Thus, it’s impossible to “miss” the latch being released.