See all articles
Thread safety in Ruby applications

Thread safety in Ruby applications

Multithreading is the ability to execute code on multiple concurrent threads. Each thread exists within a process, and each process can have at least one thread. Multithreading allows you to speed up your program, but also creates additional problems that do not occur in single-threaded programs. If you decide to use multiple threads in your app, you need to ensure that your code is thread-safe.

Let’s start with this simple program.

# program.rb
    
    class Program
      def self.call; new.call; end
    
      def call
        number = 1
        100.times { number += number }
        number
      end
    end

And a test for it, to make sure everything works properly.

# program_spec.rb
    
    require './program'
    
    describe Program do
      it { expect(described_class.call).to eq(1267650600228229401496703205376) }
    end

Time to run our test.

$ rspec program_spec.rb
    .
    
    Finished in 0.02662 seconds (files took 1.12 seconds to load)
    1 example, 0 failures

Success! Our program passed test passed. Now, let’s see what the result will be with two threads. To create a new thread, we will use the Thread class, which is an abstraction for an operating system thread. When you create a Thread, you need to pass the code block that is executed inside the thread. To ensure that the main thread will wait for our threads to finish, we have to call join method on each thread.

# program.rb
    
    class Program
      def self.call; new.call; end
    
      def call
        number = 1
        2.times.map do
          Thread.new { 500.times { number += number }}
        end.each(&:join)
        number
      end
    end

Let’s run our test.

$ rspec program_spec.rb
    .
    
    Finished in 0.02662 seconds (files took 1.12 seconds to load)
    1 example, 0 failures

Alright, it looks like everything works well. But let’s run the same example with JRuby.

$ rspec program_spec.rb
    F
    
    Failures:
    
      1) Program should eq 1267650600228229401496703205376
         Failure/Error: it { expect(described_class.call).to eq(1267650600228229401496703205376) }
    
           expected: 1267650600228229401496703205376
                got: 14855280471424563298789490688
    
           (compared using ==)
         # ./program_spec.rb:4:in `block in (root)'
    
    Finished in 0.04002 seconds (files took 1.12 seconds to load)
    1 example, 1 failure
    
    Failed examples:
    
    rspec ./program_spec.rb:4 # Program should eq 1267650600228229401496703205376

Oops, that’s not what we expected! What’s going on? What you see above happened because, unlike with MRI (Matz's Ruby Interpreter), there is no GIL (Global Interpreter Lock) mechanism in JRuby. The GIL ensures that two threads cannot be executed at the same time. That means threads on MRI won't run in parallel - MRI just switches between threads, giving each some CPU time. To fix our code to work with JRuby, we can use the Mutex locking system, which will allow to synchronize access to certain sections of code.

# program.rb
    
    class Program
      def self.call; new.call; end
    
      def call
        number = 1
        mutex = Mutex.new
        2.times.map do
          Thread.new do
            500.times { mutex.synchronize { number += number }}
          end
        end.each(&:join)
        number
      end
    end

Let’s check if it works.

$ rspec program_spec.rb
    .
    
    Finished in 0.02662 seconds (files took 1.12 seconds to load)
    1 example, 0 failures

Ok, fixed. Does that mean you don’t have to care about thread safety when you have GIL? Well, not really. Imagine a situation in which you read some global resource and update its value after a while. We can simulate this with the following example:

# program.rb
    
    class Program
      def self.call; new.call; end
    
      def call
        number = 1
        2.times.map do
          Thread.new do
            50.times do
              number_value = number
              sleep(0.001)
              number += number_value
            end
          end
        end.each(&:join)
        number
      end
    end

Now, let’s run that with MRI.

$ rspec program_spec.rb
    F
    
    Failures:
    
      1) Program should eq 1267650600228229401496703205376
         Failure/Error: it { expect(described_class.call).to eq(1267650600228229401496703205376) }
    
           expected: 1267650600228229401496703205376
                got: 1525719740468429782208
    
           (compared using ==)
         # ./program_spec.rb:4:in `block (2 levels) in <top (required)>'
    
    Finished in 0.07984 seconds (files took 0.28282 seconds to load)
    1 example, 1 failure
    
    Failed examples:
    
    rspec ./program_spec.rb:4 # Program should eq 1267650600228229401496703205376

As we can see, GIL does not ensure that our code is always thread safe. We need to make sure that all operations are atomic. We do that by using the same mechanism as before - Mutex.

# program.rb
    
    class Program
      def self.call; new.call; end
    
      def call
        number = 1
        mutex = Mutex.new
        2.times.map do |i|
          Thread.new do
            50.times do
              mutex.synchronize do
                number_value = number
                sleep(0.001)
                number += number_value
              end
            end
          end
        end.each(&:join)
        number
      end
    end

If you use threads, you need to make sure that your code and any gem you use is thread safe. Special care should be taken if you use a background processing system, like Sidekiq. By default, one Sidekiq process creates 10 threads. You can change that by changing the concurrency value in the Sidekiq config file.

Read Similar Articles