3 ways to make your ruby object thread-safe

… and check why 5600+ Rails engineers read also this

3 ways to make your ruby object thread-safe

Let’s say you have an object and you know or suspect it might be used (called) from many threads. What can you do to make it safe to use in such a way?

1. Make it stateless & frozen

Here is the most basic approach which is sometimes the easiest to go with and also very safe. Make your object state-less. In other words, forbid an object from having any long-term internal state. That means, use only local variables and no instance variables (@ivars).

To be sure that you are not accidentally adding state, you can freeze the object. That way it’s going to raise an exception if you ever try to refactor it.

class MyCommandHandler
  def initialize
    make_threadsafe_by_stateless
  end

  def call(cmd)
    local_var = cmd.something
    output(local_var)
  end

  private

  def make_threadsafe_by_stateless
    freeze
  end

  def output(local_var)
    puts(local_var)
  end
end

CMD_HANDLER = MyCommandHandler.new

Since your object does not have any local state it can be freely used between multiple threads. It’s ok for example to use the same instance when processing different requests in a threaded application server such as Puma.

You can assign MyCommandHandler.new to a global variable or pass as a dependency to objects created in different threads and things should be fine.

If someone from your team tries to refactor the code to:

class MyCommandHandler
  def initialize
    make_threadsafe_by_stateless
  end

  def call(cmd)
    @ivar = cmd.something
    output
  end

  private

  def make_threadsafe_by_stateless
    freeze
  end

  def output
    puts(@ivar)
  end
end

they are going to get an exception can't modify frozen MyCommandHandler unless they remove make_threadsafe_by_stateless in which case it’s a conscious decision, not an accidental one.

For years I struggled a bit when thinking about thread-safety and which objects can be used between threads and which can’t be and whether I need to make something thread-safe or not. Later I realized it’s not as much about a single object’s properties but rather about a graph of objects.

Imagine a situation like this:

class MyCommandHandler
  def initialize(repository, adapter)
    @repository = repository
    @adapter = adapter
    make_threadsafe_by_stateless
  end

  def call(cmd)
    obj = @repository.find(cmd.id)
    obj.do_something
    @repository.update(obj)
    @adapter.notify(SomethingHappened.new(cmd.id))
  end

  private

  def make_threadsafe_by_stateless
    freeze
  end
end

CMD_HANDLER = MyCommandHandler.new(Repository.new, Adapter.new)

If CMD_HANDLER is used between multiple threads, then its dependencies are as well. That means that thread-safety is more a property for a graph of objects (object and its dependencies and their dependencies etc) rather than a property of a single object.

In this case, it’s not enough that MyCommandHandler is stateless and thread-safe. Its dependencies should be as well for the whole solution to work properly.

2. Use thread-safe structure for local state

If you know that an object can be used between multiple threads you can compartmentalize its state per thread. For that you can use ThreadLocalVar from concurrent-ruby project:

ThreadLocalVar: Shared, mutable, isolated variable which holds a different value for each thread which has access. Often used as an instance variable in objects which must maintain different state for different threads

require 'concurrent'

class Subscribers
  def initialize
    @subscribers = Concurrent::ThreadLocalVar.new{ [] }
  end

  def add_subscriber(subscriber)
    @subscribers.value += [subscriber]
  end

  def notify
    @subscribers.value.each(&:call)
  end

  def remove_subscriber(subscriber)
    @subscribers.value -= [subscriber]
  end
end

SUBSCRIBERS = Subscribers.new

SUBSCRIBERS can be used from within different threads because its state is different for every thread that uses it. @subscribers.value is a different Array for every thread. This might be useful and what you want/expect. But it also might not.

In RailsEventStore we use this pattern to keep a list of short-term handlers interested in events published by the current thread. For example, an import process can collect stats about the number of ProductImported and ProductImportErrored events that occur when parsing and processing an XLSX file.

3. Protect the state with mutexes

In this approach, the object’s state is shared between all threads but the access is limits to a single thread at once.

require 'thread'

class Subscribers
  def initialize
    @semaphore = Mutex.new
    @subscribers = []
  end

  def add_subscriber(subscriber)
    @semaphore.synchronize do
      @subscribers += [subscriber]
    end
  end

  def notify
    @semaphore.synchronize do
      @subscribers.each(&:call)
    end
  end

  def remove_subscriber(subscriber)
    @semaphore.synchronize do
      @subscribers -= [subscriber]
    end
  end
end

SUBSCRIBERS = Subscribers.new

Although to be honest, I am not sure if synchronize is needed for a method which does not change the state such as notify… (discussion on reddit)

But instead of going that way, you might prefer to use already existing classes such as Concurrent::Array and going with the previous method.

A thread-safe subclass of Array. This version locks against the object itself for every method call, ensuring only one thread can be reading or writing at a time. This includes iteration methods like #each.

require 'concurrent'

class Subscribers
  def initialize
    @subscribers = Concurrent::Array.new
  end

  def add_subscriber(subscriber)
    @subscribers << subscriber
  end

  def notify
    @subscribers.each(&:call)
  end

  def remove_subscriber(subscriber)
    @subscribers.delete(subscriber)
  end
end

SUBSCRIBERS = Subscribers.new

4. Bonus: New instance per usage

Your object does not need to be explicitly made thread safe if:

  • you don’t share it between threads ;)
  • ie. by always creating a new instance when it’s needed
class MyCommandHandler
  def initialize(repository, adapter)
    @repository = repository
    @adapter = adapter
  end

  def call(cmd)
    @obj = @repository.find(cmd.id)
    @obj.do_something
    @repository.update(@obj)
    @adapter.notify(SomethingHappened.new(cmd.id))
  end
end

CMD_HANDLER = -> (cmd) { MyCommandHandler.new(Repository.new, Adapter.new).call(cmd) }

Here, in case our application threads use CMD_HANDLER.call(...) then we don’t need to worry about thread-safety because every time we need MyCommandHandler, we instantiate a new object with the whole dependency tree. The dependencies (repository, adapter) can use any of the mentioned techniques to be thread-safe as well, or they can be new instances as well.

And here lies the reason that many classes are not thread-safe in Ruby by default. They are simply not expected to be used from multiple threads. The authors did not imagine such use-case for them. That’s OK. The bigger issue, in my opinion, is that it’s often hard to find info about classes coming from some gems about their thread-safety.

Would you like to continue learning more?

If you enjoyed that story, subscribe to our newsletter. We share our every day struggles and solutions for building maintainable Rails apps which don’t surprise you.

You might enjoy reading:

You might also like