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 wait
return 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 CountDownLatch
:
* 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 ConditionVariable
cannot.
* 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.