Learn Concurrency by Implementing Futures in Ruby

Share this article

Abstract wallpaper with business concept

Futures are a concurrency abstraction that represents the result of an asynchronous computation. This means that when you give a future some computation to process, this is done in a separate thread. Therefore, the main thread is free to do other processing. The moment you need a result from the future, you can ask for it. If it is still processing the computation, then the main thread gets blocked. Otherwise, the result is returned.

In this article, we will implement our very own Futures library in Ruby. Along the way, you will learn more about some of the concurrency libraries that Ruby provides and some fun Ruby tricks! Let’s dive right in.

How are Futures Useful?

Before we begin, it’ll help a little to see how Futures can be useful to us. Futures are a perfect candidate for making concurrent HTTP requests. Let’s start with a simple Ruby application that fetches random Chuck Norris jokes from The Internet Chuck Norris Database:

require 'open-uri'
require 'json'
require 'benchmark'

class Chucky
  URL = 'http://api.icndb.com/jokes/random'

  def sequential
    open(URL) do |f|
      f.each_line { |line| puts JSON.parse(line)['value']['joke'] }
    end
  end

end

In order to run this application, save the above as chucky.rb and run the program like so:

% irb
> require "./chucky"
 => true
> chucky = Chucky.new
 => #<Chucky:0x007fe02c046d98>
> chucky.sequential
Contrary to popular belief, the Titanic didn't hit an iceberg. The ship was off course and ran into Chuck Norris while he was doing the backstroke across the Atlantic.

Each time you execute chucky.sequential, the program will fetch a random Chuck Norris joke. (Warning: This is highly addictive!) What happens if we wanted to fetch more than, say, ten jokes? A naive solution looks something like:

10.times { chucky.sequential }

Unfortunately, this is an extreme waste of CPU resources and your time. While each request is made, the main thread is blocked and has to wait for the request to complete before going on to the next one. We are going to fix that by implementing our own Futures abstraction (as all good developers do).

Implementing Your Own Futures: Test First!

We are going to implement our Futures gem using Test-Driven Development (TDD). Let’s do this! We begin by creating a new Ruby gem using bundle gem <gem name>:

% bundle gem futurama
Creating gem 'futurama'...
MIT License enabled in config
      create  futurama/Gemfile
      create  futurama/.gitignore
      create  futurama/lib/futurama.rb
      create  futurama/lib/futurama/version.rb
      create  futurama/futurama.gemspec
      create  futurama/Rakefile
      create  futurama/README.md
      create  futurama/bin/console
      create  futurama/bin/setup
      create  futurama/LICENSE.txt
      create  futurama/.travis.yml
      create  futurama/.rspec
      create  futurama/spec/spec_helper.rb
      create  futurama/spec/futurama_spec.rb
Initializing git repo in futurama

Next, run bin/setup to bring in the dependencies:

% bin/setup
Resolving dependencies...
Using rake 10.4.2
Using bundler 1.10.6
Using diff-lcs 1.2.5
Using futurama 0.1.0 from source at .
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.

Note: If you don’t see the RSpec gems install, add spec.add_development_dependency "rspec" to the futurama.gemspec file.

First Steps

Our first test is deliberately simple:

require 'spec_helper'
require 'timeout'

module Futurama
  describe 'Future' do

    it 'returns a value' do
      future = Future.new { 1 + 2 }

      expect(future).to eq(3)
    end

  end

end

This test describes the interface of creating a Future. The Future object takes in a block of computation. Also, When the object is accessed, the return value is the result of the computation within that block (This second condition is not as simple as it sounds).

Next, create a file called future.rb in lib/futurama/future.rb. Next, make sure you require future.rb in lib/futurama.rb like so:

require "futurama/version"
require "futurama/future"

module Futurama
end

In order to pass the test, the Future object must:

  • accept a block
  • return the value of the block when it is invoked

Satisfying the first condition is easy enough:

module Futurama
  class Future

    def initialize(&block)
      @block = block
    end

  end
end

The other condition is slightly trickier:

require 'delegate'

module Futurama
  class Future < Delegator

    # initialize was here

    def __getobj__
      @block.call
    end

  end
end

When we subclass the built-in Delegator class, we must implement the __getobj__ method, otherwise Ruby will complain. But then again, this is the whole point of using the Delegator class! The return value of this method is basically what is returned when the object is accessed. In other words, we can override what it means when an object is accessed, which is exactly what we need! We invoke the block from the __getobj__ method and when we run the tests:

% rspec
Future
  returns a value

Finished in 0.00077 seconds (files took 0.06902 seconds to load)
1 example, 0 failures

Great success!

Executing Computations in the Background

Back to our tests. A Future takes a computation and runs it in a thread. In order to test this out, we can create a Future and let it sleep for one second before returning a value. On the main thread, we also simulate some computation that takes for a second:

module Futurama
  describe 'Future' do

    it 'executes the computation in the background' do
      future = Future.new { sleep(1); 42 }

      sleep(1) # do some computation

      Timeout::timeout(0.9) do
        expect(future).to eq(42)
      end
    end

  end
end

What we assert is interesting. Since the Future is running in the background just as the main thread sleeps for a second, it should in theory take less than 1 second before the result from the future is returned. We make use of the built-in Timeout library (we did a require 'timeout' at the top of spec/futurama/future.rb) to make sure that the future executes within the time boundary we set.

module Futurama
  class Future < Delegator

    def initialize(&block)
      @block  = block
      @thread = Thread.new { run_future }
    end

    def run_future
      @block.call
    end

    def __getobj__
      @thread.value
    end

  end
end

We wrap the calling of the block in the run_future method and run_future in a thread. This thread runs the moment the Future is created. Once the thread completes, the return value is accessed using Thread#value, as seen in the modified implementation of __getobj__.

Run the tests and everything should be green.

Handling Exceptions

Next, we turn our attention to handling exceptions. Since Futures are asynchronous, we do not want any nasty surprises should a Future suddenly fail. Instead, it would be nice for the future to store the exception and only raise it when we poke it. Here is the test that expresses the intent:

module Futurama
  describe 'Future' do

    it 'captures exceptions and re-raises them' do
      error_msg = 'Good news, everyone!'

      future = Future.new { raise error_msg }

      expect { future.inspect }.to raise_error RuntimeError, error_msg
    end

  end
end

Happily, you don’t have to do anything to get the test to pass! There’s a caveat though. If Thread.abort_on_exception is set to true, unhandled exceptions in any thread will cause the interpreter to exit. Not fun. Let’s expose this problem in the test:

module Futurama
  describe 'Future' do

    it 'captures exceptions and re-raises them' do
      Thread.abort_on_exception = true

      error_msg = 'Good news, everyone!'

      future = Future.new { raise error_msg }

      expect { future.inspect }.to raise_error RuntimeError, error_msg
    end

  end
end

With this new line, the test no longer passes. What to do? Turns out, you do need to do more work. (Sorry!) Here’s where we come to the meat of our implementation.

Queues!

From the previous section, we now have to find a way to store exceptions when they happen, instead of relying on the interpreter. Also, we need to re-raise the exception when the future object is invoked.

Before we get into the implementation, let’s think about the Future again. In one sense, it is a data structure that stores either the resolved value or an exception. Another very important consideration is making sure of thread-safety. How can we represent this??

While Ruby doesn’t have many thread-safe collection classes, Queue is an exception. From the class’ description:

This class provides a way to synchronize communication between threads.

For our implementation, recall that we are either storing the resolved value or the exception. Therefore, we need a Queue that has a maximum size of one. SizedQueue to the rescue!

module Futurama
  class Future < Delegator

    def initialize(&block)
      @block  = block
      @queue  = SizedQueue.new(1)
      @thread = Thread.new { run_future }
    end

    def run_future
      @queue.push(value: @block.call)
    rescue Exception => ex
      @queue.push(exception: ex)
    end

    def __getobj__
      # this will change in the next section
    end

  end
end

A new instance variabl, @queueis added, which is a SizedQueue with a capacity of one. We then modify run_future to either push the result of the block or an exception if one occurs. Because we are using a SizedQueue, we are guaranteed that the queue will not have two elements being pushed into it.

Getting the Result or Exception from the Queue

Next, we need to tackle the issue of getting the result or exception from the SizedQueue. Another thing to keep in mind is that once a Future resolves a value, it is done. The next time you get a value out of the Future, the result is going to be immediate. In other words, the Future remembers the value/exception once it is resolved.

module Futurama
  class Future < Delegator

    def initialize(&block)
      @block  = block
      @queue  = SizedQueue.new(1)
      @thread = Thread.new { run_future }
      @mutex  = Mutex.new
    end

    def __getobj__
      resolved_future_or_raise[:value]
    end

    def resolved_future_or_raise
      @resolved_future || @mutex.synchronize do
        @resolved_future ||= @queue.pop
      end

      Kernel.raise @resolved_future[:exception] if @resolved_future[:exception]
      @resolved_future
    end

  end
end

Let’s concentrate on resolved_future_or_raise method:

@resolved_future || @mutex.synchronize do
  @resolved_future ||= @queue.pop
end

Here, irst check if the Future has been resolved. That is just a fancy way of saying that the Future has completed computing a value or exception. Otherwise, retrieve this value/exception from @queue. We need to make sure that the operations of popping the queue and assigning the result to @resolved_future is performed atomically. In other words, we must guarantee that interleavings never happen. Therefore, we wrap the operations in a @mutex:

Kernel.raise @resolved_future[:exception] if @resolved_future[:exception]
@resolved_future

Now,check if @resolved_future has an exception. If so, raise it. Note that we are using Kernel#raise. Without specifying Kernel, Thread#raise would be used.

Finally, if there are no exceptions, the resolved value will be returned. Run the tests again, and everything should be a sweet-colored green!

Saving on Keystrokes (or: Polluting the Kernel namespace)

Having to type Futurama::Future.new { } is no fun. What if we could just future{ } instead. In Ruby, this is trivial. Let’s write a test for this first:

module Futurama
  describe 'Future' do
    it 'pollutes the Kernel namespace' do

      msg    = 'Do the Bender!'
      future = future { msg }

      expect(future).to eq(msg)
    end

  end
end

Run the tests, and it will fail with:

NoMethodError: undefined method `future' for #<RSpec::ExampleGroups::Future:0x007fd274988390>
./spec/futurama/future_spec.rb:67:in `block (2 levels) in <module:Futurama>'
-e:1:in `load'
-e:1:in `<main>'

We can easily fix this by creating a file called kernel.rb in lib/futurama:

require 'futurama'

module Kernel
  def future(&block)
    Futurama::Future.new(&block)
  end
end

Add a require statement for this file to lib/futurama.rb and run the tests again. We are back to green!

Getting the Value or Exception from the Future

Currently, the value or exception can be accessed from __getobj__. Obviously, we do not expect the client code to know about __getobj__. We can instead alias this to something like value:

module Futurama
  describe 'Future' do

    it 'allows access to its value' do
      val    = 10
      future = Future.new { val }

      expect(future.value).to eq(val)
    end

  end
end

Unsurprisingly, the test will fail with:

NoMethodError: undefined method `value' for 10:Futurama::Future
./spec/futurama/future_spec.rb:76:in `block (2 levels) in <module:Futurama>'
-e:1:in `load'
-e:1:in `<main>'

The code to get the test to pass is a one-liner:


require 'thread'

module Futurama
  class Future < Delegator

  def __getobj__
    resolved_future_or_raise[:value]
  end

  # place this *below* __getobj__
  alias_method :value, :__getobj__
  end
end

Everything should be green now!

Let’s Have Some Concurrent Chuck Norris Jokes!

Let’s take a look at chucky.rb again. Note that I have placed this file in sample/chucky.rb of futurama.

require '../lib/futurama'
require 'open-uri'
require 'json'
require 'benchmark'

class Chucky
  URL = 'http://api.icndb.com/jokes/random'

  def sequential
    open(URL) do |f|
      f.each_line { |line| puts JSON.parse(line)['value']['joke'] }
    end
  end

  def concurrent
    future { sequential }
  end

end

It only needs one tiny change to perform concurrent Chuck Norris joke lookup:

def concurrent
  future { sequential }
end

That’s it! It might come across as a bit anti-climatic, but we’re pretty much done. In order to put this to the test, we can benchmark the concurrent version versus the sequential one:


chucky = Chucky.new

Benchmark.bm do |x|
  x.report('concurrent') { 10.times { chucky.concurrent } }
  x.report('sequential') { 10.times { chucky.sequential } }
end

Or, if you are too lazy, you can see the results on my machine:

Limitations, Acknowledgments, and Where to Learn More

Creating an unbounded number of Futures will lead to the creation of an unbounded number of threads. This is obviously a bad thing. Languages like Java have Thread Pool Executors which manage a pool of threads. It is not extremely difficult to implement one in Ruby.

This article was inspired by studying the source code of the Futuroscope gem. In fact, its implementation has a thread pool that is passed into the constructor of the Future.

If you are interested in learning more about concurrency abstractions built in Ruby, look no further than the Concurrent Ruby gem. It consists of a plethora of concurrency tools like Agents, Thread Pools, Supervisors and, yes, Futures too! The documentation provided is highly readable too.

Thanks for Reading!

I hope you had fun learning about Futures, and hopefully more about Ruby. While Ruby, by default, doesn’t come with a mature library for concurrency, gems such as Concurrency Ruby provide an excellent avenue to learn about these concurrency tools in your favorite language.

Frequently Asked Questions (FAQs) about Concurrency and Futures in Ruby

What is the difference between concurrency and parallelism in Ruby?

Concurrency and parallelism are two terms often used interchangeably, but they have different meanings. Concurrency in Ruby refers to the ability of a program to be broken into parts that can run independently of each other, but not necessarily at the same time. It’s about dealing with a lot of things at once. On the other hand, parallelism is about doing a lot of things at the same time. It involves executing multiple threads or processes simultaneously. Ruby supports both concurrency and parallelism, but the latter is only possible on certain platforms due to the Global Interpreter Lock (GIL).

How does the Future class in Ruby work?

The Future class in Ruby is a part of the concurrent-ruby gem. It represents a value that may not have been computed yet. Futures are used to execute code in parallel, and they provide a way to access the result of the computation when it’s ready. When you create a Future, you provide a block of code that will be executed in a separate thread. The Future object immediately returns, and you can continue executing other code. When you need the result of the Future, you call its value method, which will block until the result is ready.

What is the difference between Future and Promise in Ruby?

Both Future and Promise are used for handling asynchronous operations in Ruby, but they have some differences. A Future represents a value that will be computed in the future. It starts its computation immediately when it’s created. On the other hand, a Promise doesn’t start its computation until its value is requested. Also, a Promise can be fulfilled or rejected manually, while a Future cannot.

How can I handle exceptions in Futures in Ruby?

When an exception occurs in the block of code executed by a Future, it’s stored in the Future object. When you call the value method on the Future, the stored exception is raised. You can handle this exception using a standard rescue block. If you want to check if a Future has an exception without raising it, you can use the reason method, which returns the exception if there is one, or nil if there isn’t.

Can I use Futures in Ruby on Rails?

Yes, you can use Futures in Ruby on Rails. The concurrent-ruby gem, which provides the Future class, is thread-safe and can be used in a Rails application. However, keep in mind that Rails has its own mechanisms for handling concurrency and parallelism, such as Active Job and Action Cable. Depending on your use case, these might be more appropriate.

How can I test code that uses Futures in Ruby?

Testing code that uses Futures can be challenging because of the asynchronous nature of Futures. However, the concurrent-ruby gem provides some tools to help with this. For example, you can use the Future#execute method to force the Future to execute its block of code immediately, which can be useful in tests.

Can I use Futures with other concurrency abstractions in Ruby?

Yes, Futures can be used with other concurrency abstractions provided by the concurrent-ruby gem, such as Actors and Channels. These abstractions can be combined in various ways to handle complex concurrency scenarios.

How can I cancel a Future in Ruby?

Currently, there is no direct way to cancel a Future in Ruby. Once a Future has started executing its block of code, it cannot be stopped. However, you can design your code in a way that it can check for cancellation conditions and stop executing if necessary.

Can I use Futures in Ruby without the concurrent-ruby gem?

While the Future class is provided by the concurrent-ruby gem, the concept of futures can be implemented in Ruby without this gem. You can use Ruby’s built-in Thread class to create your own futures. However, the concurrent-ruby gem provides a lot of additional features and safety measures that make working with futures easier and safer.

How does the Future class in Ruby handle thread safety?

The Future class in Ruby is designed to be thread-safe. It uses synchronization mechanisms to ensure that its state can be safely accessed and modified from multiple threads. This means that you can safely use Future objects in multi-threaded code without worrying about thread safety issues.

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.

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