Using Class Instance Variable for Mutex in Ruby

Should class instance variables in Rails be set within a mutex?

It's possible (and desirable) to run Rails in a multi-threaded mode even in MRI. This can be accomplished by changing a line in production.rb.

config.threadsafe!

In MRI, two threads cannot run code simultaneously, but a context switch can happen at any time. In Rubinius and JRuby, threads can run code simultaneously.

Let's look at the code you showed:

class Something
def self.objects
@objects ||= begin
# some logic that builds an array, which is ultimately stored in @objects
end
end
end

The ||= code gets expanded to something like:

class Something
def self.objects
@objects || (@objects = begin
# some logic that builds an array, which is ultimately stored in @objects
end)
end
end

This means that there are actually two steps to the process:

  1. look up @objects
  2. If @objects is falsy, set @objects to the results of the begin/end expression

It may be possible for the context to switch between these steps. It is certainly possible for the context to switch in the middle of step 2. This means that you may end up running the block multiple times instead of once. In MRI, this may be acceptable, but it's perfectly straight forward to lock a mutex around the expression, so do it.

class Something
MUTEX = Mutex.new

def self.objects
MUTEX.synchronize do
@objects ||= begin
# some logic that builds an array, which is ultimately stored in @objects
end
end
end
end

Thread Safety: Class Variables in Ruby

Instance variables are not thread safe (and class variables are even less thread safe)

Example 2 and 3, both with instance variables, are equivalent, and they are NOT thread safe, like @VincentXie stated. However, here is a better example to demonstrate why they are not:

class Foo
def self.bar(message)
@bar ||= message
end
end

t1 = Thread.new do
puts "bar is #{Foo.bar('thread1')}"
end

t2 = Thread.new do
puts "bar is #{Foo.bar('thread2')}"
end

sleep 2

t1.join
t2.join

=> bar is thread1
=> bar is thread1

Because the instance variable is shared amongst all of the threads, like @VincentXie stated in his comment.

PS: Instance variables are sometimes referred to as "class instance variables", depending on the context in which they are used:

When self is a class, they are instance variables of classes(class
instance variables). When self is a object, they are instance
variables of objects(instance variables). - WindorC's answer to a question about this

Rails class method is thread safe?

This in an instance method, not to be confused with a class method. The answers method is on an instance of User, as opposed to being on the User class itself. This method is caching the answers on the instance of a User, but as long as this User instance is being instantiated with each web request (such as a User.find()or User.find_by()), you’re fine because the instance is not living between threads. It’s common practice to look records up every web request in the controller, so you’re likely doing that.

If this method was on the User class directly (such as User.answers), then you’d need to evaluate whether it’s safe for that cached value to be maintained across threads and web requests.

To recap, the your only concern for thread safety is class methods, class variables (instance variables that use two at signs such as @@answers), and instance methods where the instance lives on past a single web request.

If you ever find yourself needing to use a class-level variable safely, you can use Thread.current, which is essentially a per-thread Hash (like {}) that you can store values in. For example Thread.current[:foo] = 1 would be an example. ActiveSupport uses this when setting Time.zone.

Alternatively you may find times where you need a single array that you need to safely share across threads, in which case you’d need to look into Mutex, which basically lets you have an array that you lock and unlock to give threads safe access to reading and writing in it. The Sidekiq gem uses a Mutex to manage workers, for example. You lock the Mutex, so that no one else can change it, then you write to it, and then unlock it. It’s important to note that if any other thread wants to write to the Mutex while it’s locked, it’ll have to wait for it to become unlocked (like, the thread just pauses while the other thread writes), so it’s important to lock as short as possible.

Setting instance variables on different objects of the same class

Never mind, I just discovered instance_variable_set.

class Person

attr_reader :partner

def partner=(person)

# reset the old partner instance variable if it exists
partner.instance_variable_set(:@partner, nil) if partner

# set the partner attributes
@partner = person
person.instance_variable_set(:@partner, self)
end
end


Related Topics



Leave a reply



Submit