A Countdown Latch is a concurrency abstraction that allows one or more threads to wait until all other threads are done with what they are doing. Therefore, a countdown latch is often referred to as a thread synchronization primitive.
How are Countdown Latches Useful?
Let say you have a bunch of threads that are fetching, say, Chuck Norris jokes. You create ten threads and you want to ensure that they all complete before you proceed on with the next step.
In this instance, you can create a countdown latch with a counter of ten. Then, right before your next step in the code, you can wait for your threads to complete. Until then, your code cannot proceed until all the threads are done.
When each thread fetches a joke, it will decrement the counter. Eventually as all ten threads are done fetching jokes, the counter in the countdown latch will eventually hit zero. That’s when the code is allowed to proceed.
Implementing Your Own Countdown Latch: Test First!
In a previous article, we went though how to implement Futures in Ruby. In this article, we are going test-drive an implementation of a Countdown Latch. Let’s get started!
Setting Up
Let’s bootstrap a new Ruby project. My favorite way is to create a Ruby gem:
% bundle gem countdown_latch -t
Creating gem 'countdown_latch'...
MIT License enabled in config
create countdown_latch/Gemfile
create countdown_latch/.gitignore
create countdown_latch/lib/countdown_latch.rb
create countdown_latch/lib/countdown_latch/version.rb
create countdown_latch/countdown_latch.gemspec
create countdown_latch/Rakefile
create countdown_latch/README.md
create countdown_latch/bin/console
create countdown_latch/bin/setup
create countdown_latch/LICENSE.txt
create countdown_latch/.travis.yml
create countdown_latch/.rspec
create countdown_latch/spec/spec_helper.rb
create countdown_latch/spec/countdown_latch_spec.rb
Initializing git repo in /Users/benjamintan/workspace/countdown_latch
Did you notice the
-t
flat appended? This flag adds RSpec as a development dependency.
Next, go into the project directory:
% cd countdown_latch
Run bin/setup
to install the dependencies:
% bin/setup
Resolving dependencies...
Using rake 10.4.2
Using bundler 1.10.6
Using countdown_latch 0.1.0 from source at .
Using diff-lcs 1.2.5
Using rspec-support 3.3.0
Using rspec-core 3.3.2
Using rspec-expectations 3.3.1
Using rspec-mocks 3.3.2
Using rspec 3.3.0
Bundle complete! 4 Gemfile dependencies, 9 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
Creating the Countdown Latch
Now, on to the fun stuff! Since we are doing this test-first. The tests will help us drive towards the implementation that we will flesh out in order to get the tests to pass.
As a sanity check, we can quickly make sure everything is hooked up as expected:
% rspec
CountdownLatch
has a version number
does something useful (FAILED - 1)
Failures:
1) CountdownLatch does something useful
Failure/Error: expect(false).to eq(true)
expected: true
got: false
(compared using ==)
# ./spec/countdown_latch_spec.rb:9:in `block (2 levels) in <top (required)>'
Finished in 0.01957 seconds (files took 0.07757 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/countdown_latch_spec.rb:8 # CountdownLatch does something useful
Excellent! RSpec is working fine! Now, open up the test file located at spec/countdownlatchspec.rb. We are going to write the first test.
A Countdown Latch Requires a Non-negative Integer as an Argument
You should delete everything in spec/countdownlatchspec.rb and replace it with this skeleton:
module CountdownLatch
describe CountdownLatch do
end
end
The first test is to make sure that the argument passed into the constructor of the countdown latch is a non-negative integer:
module CountdownLatch
describe CountdownLatch do
it "requires a non-negative integer as an argument" do
latch = CountdownLatch.new(3)
expect(latch.count).to eq(3)
end
end
end
In order to get this to pass, we need a constructor that accepts a non-negative integer as an argument. Getting this test to pass is simple:
module CountdownLatch
class CountdownLatch
def initialize(count)
if count.is_a?(Fixnum) && count > 0
@count = count
end
end
end
end
At this point, the test will pass. Let’s also make sure that 0 is accepted:
module CountdownLatch
describe CountdownLatch do
it "zero is a valid argument" do
latch = CountdownLatch.new(0)
expect(latch.count).to eq(0)
end
end
end
Whoops! Our test caught something:
Failures:
1) CountdownLatch::CountdownLatch zero is a valid argument
Failure/Error: expect(latch.count).to eq(0)
expected: 0
got: nil
(compared using ==)
Turns out, we have an off by one error when comparing count
and zero:
module CountdownLatch
class CountdownLatch
def initialize(count)
if count.is_a?(Fixnum) && count >= 0
@count = count
end
end
end
end
The countdown latch cannot be initialized with a negative number, or a Float
, for example, since those two cases do not make sense. Let’s add the tests:
module CountdownLatch
describe CountdownLatch do
it "throws ArgumentError for negative numbers" do
expect { CountdownLatch.new(-1) }.to raise_error(ArgumentError)
end
it "throws ArgumentError for non-integers" do
expect { CountdownLatch.new(1.0) }.to raise_error(ArgumentError)
end
end
end
Now the tests will fail:
Failures:
1) CountdownLatch::CountdownLatch throws ArgumentError for negative numbers
Failure/Error: expect { CountdownLatch.new(-1) }.to raise_error(ArgumentError)
expected ArgumentError but nothing was raised
# ./spec/countdown_latch_spec.rb:17:in `block (2 levels) in <module:CountdownLatch>'
2) CountdownLatch::CountdownLatch throws ArgumentError for non-integers
Failure/Error: expect { CountdownLatch.new(1.0) }.to raise_error(ArgumentError)
expected ArgumentError but nothing was raised
# ./spec/countdown_latch_spec.rb:21:in `block (2 levels) in <module:CountdownLatch>'
Thankfully, this too is an easy fix. We just have to get the initializer to raise an ArgumentError
for well, errors in the argument. In lib/countdownlatch/countdownlatch.rb:
module CountdownLatch
class CountdownLatch
def initialize(count)
if count.is_a?(Fixnum) && count >= 0
@count = count
@mutex = Mutex.new
@condition = ConditionVariable.new
else
raise ArgumentError
end
end
end
end
Now the tests should all pass.
Counting Down
A countdown latch has to know how to count down its own internal counter.
module CountdownLatch
describe CountdownLatch do
it "#count decreases when #count_down is called" do
latch = CountdownLatch.new(3)
latch.count_down
expect(latch.count).to eq(2)
end
end
end
Now we need have access to the @count
field. That is easy enough:
module CountdownLatch
class CountdownLatch
def initialize(count)
# ...
end
def count
@count
end
end
end
The count_down
method looks almost simplistic:
module CountdownLatch
class CountdownLatch
def count_down
@count -= 1
end
end
end
Resist the temptation to add more behavior and “smarter” logic until the tests are forcing you to do something about it. In this case, the tests should be all green now.
However, our tests are not complete. The countdown latch should only count down to zero and nothing less. Let’s add a test for that:
module CountdownLatch
describe CountdownLatch do
it "#count never reaches below zero" do
latch = CountdownLatch.new(0)
latch.count_down
expect(latch.count).to eq(0)
end
end
end
The above test fails:
1) CountdownLatch::CountdownLatch #count never reaches below zero
Failure/Error: expect(latch.count).to eq(0)
expected: 0
got: -1
(compared using ==)
We just need to check if @count
is zero:
module CountdownLatch
class CountdownLatch
def count_down
unless @count.zero?
@count -= 1
end
end
end
end
Everything should be green!
Awaiting for Threads
Till now, we simply have a glorified countdown object. It is not even thread-safe! This is because count_down
can be called by multiple threads and since @count
is not synchronized in anyway, race conditions can occur.
This is the first test that will drive us to implement the concurrency features in our countdown latch:
module CountdownLatch
describe CountdownLatch do
it "#await will wait for a thread to finish its work" do
latch = CountdownLatch.new(1)
Thread.new do
latch.count_down
end
latch.await
expect(latch.count).to eq(0)
end
end
end
The test creates a countdown latch initialized to 1
. We then create a thread that decrements the latch. Outside of the thread, we call latch.await
.
This is basically saying that the program will wait for the thread to finish its work. Again, once the thread is done with its work, it will call latch.count_down
. Therefore, we expect that the count
of the latch to be zero.
In order to see the test fail correctly, we need an empty implementation of await
:
module CountdownLatch
class CountdownLatch
def await
end
end
end
When you run the tests, the following will fail:
Failures:
1) CountdownLatch::CountdownLatch #await will wait for a thread to finish its work
Failure/Error: expect(latch.count).to eq(0)
expected: 0
got: 1
(compared using ==)
Since await
does nothing, the program doesn’t wait for the thread to tell the countdown latch that it’s OK to proceed. Instead, the program simply runs straight through. In order to get this test to pass, you need to know about condition variables.
Condition Variables
A condition variable is essentially a synchronization primitive (recall that a countdown latch is also a synchronization primitive) that allows threads to wait until some condition occurs. In the case of a countdown latch, that condition is when @count
hits zero.
When that happens, the broadcast
method has to be called on the condition variable to tell all the waiting threads that they can stop waiting and proceed.
First, we’ll add the condition variable to the implementation and tell it to broadcast to all waiting threads when the condition has been met. Do not forget to include require "thread"
too:
require "thread" # <----
module CountdownLatch
class CountdownLatch
def initialize(count)
if count.is_a?(Fixnum) && count >= 0
@count = count
@condition = ConditionVariable.new # <----
else
raise ArgumentError
end
end
def count_down
unless @count.zero?
@count -= 1
else
@condition.broadcast # <----
end
end
end
end
Now, to deal with the other side of the equation: Making threads suspend execution and wait till the condition is met. Here’s the first attempt. ConditionVariable
has a method called wait
:
module CountdownLatch
class CountdownLatch
def await
@condition.wait
end
end
end
However, this doesn’t work because ConditionVariable#wait
requires an argument that takes in a Mutex
object. A mutex is essentially a lock that protects a section of code from being entered by more than one thread. Recall that I mentioned @count
is , until now, not thread-safe. We are going to fix this.
In this case, we need to supply a mutex to the condition variable, so that only one thread can read/write to the condition variable at any one time.
First, we will create a mutex for the countdown latch:
module CountdownLatch
class CountdownLatch
def initialize(count)
if count.is_a?(Fixnum) && count >= 0
@count = count
@condition = ConditionVariable.new
@mutex = Mutex.new <----
else
raise ArgumentError
end
end
end
end
Next, we will use @mutex.synchronize
to demarcate a critical section. A critical section is an area of code where only one thread can enter. First, let’s handle the count_down
method:
module CountdownLatch
class CountdownLatch
def count_down
@mutex.synchronize {
unless @count.zero?
@count -= 1
else
@condition.broadcast
end
}
end
end
end
We finally have a mutex to pass into @condition.wait
:
module CountdownLatch
class CountdownLatch
def await
@condition.wait(@mutex)
end
end
end
However, just like the count_down
method, you need a critical section. Here it is:
module CountdownLatch
class CountdownLatch
def await
@mutex.synchronize {
@condition.wait(@mutex)
}
end
end
end
While we are add it, let’s wrap the @count
in the count
method with a mutex too:
module CountdownLatch
class CountdownLatch
def count
@mutex.synchronize {
@count
}
end
end
end
Run the test again and we should be green! Woot!
A Sample Run
Create a folder under lib called sample. In that folder, I create a file called chucky.rb with the following contents:
require 'open-uri'
require 'json'
module CountdownLatch
class Chucky
URL = 'http://api.icndb.com/jokes/random'
def get_fact
open(URL) do |f|
f.each_line { |line| puts JSON.parse(line)['value']['joke'] }
end
end
def get_facts(num)
latch = CountdownLatch.new(num) # <---- Latch
facts = []
(1..num).each do |x|
Thread.new do
facts << get_fact
latch.count_down
end
end
latch.await
facts
end
end
end
This class fetches jokes from a third-party API, parses the JSON response and returns the joke as a String
. We can use a test to drive Chucky
and get us some facts:
require 'spec_helper'
require 'sample/chucky' # <---- Remember to add this!
module CountdownLatch
describe CountdownLatch do
it "sample run", :speed => 'slow' do
chucky = Chucky.new
facts = chucky.get_facts(5)
expect(facts.size).to eq(5)
end
end
end
As expected, the tests will pass. Try removing the latch and see what happens.
Limitations, Acknowledgments, and Where to Learn More
This implementation doesn’t handle spurious wakeups, an interesting phenomenon where threads can wake up even though the condition variable hasn’t signaled/broadcasted yet.
If you want a more solid and refined implementation of a countdown latch, take a look at the fantastic Ruby Concurrency GitHub repository.
Thanks for Reading!
I hope you have learned something new about condition variables, mutexes, and, of course, creating your own countdown latch! More importantly, I hope you had lots of fun following along. You can grab the full source here. Thanks for reading!
Frequently Asked Questions (FAQs) about Concurrency and Countdown Latch in Ruby
What is concurrency in Ruby and why is it important?
Concurrency in Ruby is a concept that allows multiple tasks to run simultaneously. It’s important because it can significantly improve the performance of your program, especially when dealing with IO-bound tasks or tasks that require waiting for external resources. Concurrency can make your program more efficient and responsive, as it can perform multiple tasks at the same time rather than waiting for one task to finish before starting the next.
How does a countdown latch work in Ruby?
A countdown latch in Ruby is a synchronization tool used in concurrent programming. It allows one or more threads to wait until a set of operations being performed in other threads completes. A countdown latch is initialized with a given count. This count is decremented by calls to the count_down method. Threads calling await will block until the current count reaches zero.
What are the benefits of using a countdown latch in Ruby?
Using a countdown latch in Ruby can help manage and control the execution of concurrent threads. It allows you to ensure that certain operations do not proceed until other operations complete, providing a way to synchronize the activities of multiple threads. This can be particularly useful in scenarios where you need to break a problem into smaller tasks that can be executed concurrently, but you need to wait for all tasks to complete before proceeding.
How can I implement a countdown latch in Ruby?
Implementing a countdown latch in Ruby involves creating a CountdownLatch class with an initialize method that sets the initial count, a count_down method that decrements the count, and an await method that blocks until the count reaches zero. You can then create an instance of this class and use it to control the execution of your threads.
Can I use a countdown latch for time-sensitive tasks in Ruby?
Yes, a countdown latch can be used for time-sensitive tasks in Ruby. By using a countdown latch, you can ensure that a certain task does not start until a specified amount of time has passed or until certain conditions have been met. This can be useful in scenarios where you need to delay the execution of a task or synchronize tasks that are dependent on each other.
What are some common pitfalls when working with concurrency in Ruby?
Some common pitfalls when working with concurrency in Ruby include race conditions, deadlocks, and thread safety issues. Race conditions occur when the output of a program depends on the relative timing of threads, while deadlocks occur when two or more threads are unable to proceed because each is waiting for the other to release a resource. Thread safety issues can arise when multiple threads access shared data, and the outcome of the threads is not deterministic.
How can I avoid race conditions when using concurrency in Ruby?
To avoid race conditions when using concurrency in Ruby, you can use synchronization mechanisms such as mutexes, semaphores, or countdown latches. These tools can help ensure that threads do not interfere with each other and that operations are performed in the correct order.
What are some best practices for working with concurrency in Ruby?
Some best practices for working with concurrency in Ruby include understanding the basics of threads and processes, using synchronization tools appropriately, avoiding shared mutable state, and testing your code thoroughly. It’s also important to understand the limitations and characteristics of the Ruby interpreter you are using, as this can affect the behavior of your concurrent code.
Can I use countdown latches with other synchronization tools in Ruby?
Yes, countdown latches can be used in conjunction with other synchronization tools in Ruby. For example, you might use a countdown latch to wait for a set of threads to complete, and a mutex to ensure that only one thread can access a certain resource at a time. Using multiple synchronization tools together can provide greater control over the execution of your concurrent code.
How can I learn more about concurrency and countdown latches in Ruby?
There are many resources available to learn more about concurrency and countdown latches in Ruby. The Ruby documentation is a great place to start, as it provides detailed information about the language’s concurrency features. There are also many online tutorials, blog posts, and books available on the subject. Additionally, experimenting with writing and running concurrent code can be a great way to learn.
Benjamin is a Software Engineer at EasyMile, Singapore where he spends most of his time wrangling data pipelines and automating all the things. He is the author of The Little Elixir and OTP Guidebook and Mastering Ruby Closures Book. Deathly afraid of being irrelevant, is always trying to catch up on his ever-growing reading list. He blogs, codes and tweets.