Learn Concurrency by Making a Countdown Latch in Ruby

Share this article

Learn Concurrency by Making a Countdown Latch in Ruby

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 Tan Wei HaoBenjamin Tan Wei Hao
View Author

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.

concurrencyGlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week