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: