51

Performing writes/reads on class variables in Ruby is not thread safe. Performing writes/reads on instance variables appears to be thread safe. That said, is it thread safe to perform write/reads on instance variables of a class or metaclass object?

What are the differences between these three (contrived) examples in terms of thread safety?

EXAMPLE 1: MUTUAL EXCLUSION

class BestUser # (singleton class)
  @@instance_lock = Mutex.new

  # Memoize instance
  def self.instance
    @@instance_lock.synchronize do
      @@instance ||= best
    end
  end
end

EXAMPLE 2: INSTANCE VARIABLE STORAGE

class BestUser # (singleton class)
  # Memoize instance
  def self.instance
    @instance ||= best
  end
end

EXAMPLE 3: INSTANCE VARIABLE STORAGE ON METACLASS

class BestUser # (singleton class)
  # Memoize instance
  class << self
    def instance
      @instance ||= best
    end
  end
end
Joseph Ravenwolfe
  • 6,480
  • 6
  • 31
  • 31

3 Answers3

19

Examples 2 and 3 are exactly the same. Modules and classes are also objects, and defining a singleton method on a object actually defines it on its singleton class.

With that said, and since you have already established instance variable access is thread safe, examples 2 and 3 are thread safe. Example 1 should also be thread safe, but it is inferior to the other two because it requires manual variable synchronization.

However, if you need to take advantage of the fact that class variables are shared within the inheritance tree, you may have to use the first approach.


The inherent thread safety of the Ruby language depends on the implementation.

MRI, before 1.9, implemented threads at the VM level. This means that even though Ruby is capable of scheduling code execution, nothing is really running in parallel within a single Ruby process. Ruby 1.9 uses native threads synchronized with a global interpreter lock. Only the context which holds the lock may execute code.

n, x = 10, 0

n.times do
  Thread.new do
    n.times do
      x += 1
    end
  end
end

sleep 1
puts x
# 100

The value of x is always consistent on MRI. On JRuby, however, the picture changes. Multiple executions of the same algorithm yielded the values 76, 87, 98, 88, 94. The result could be anything because JRuby uses Java threads, which are real threads and execute in parallel.

Just like in the Java language, manual synchronization is required in order to safely use threads in JRuby. The following code always results in consistent values for x:

require 'thread'
n, x, mutex = 10, 0, Mutex.new

n.times do
  Thread.new do
    n.times do
      mutex.synchronize do
        x += 1
      end
    end
  end
end

sleep 1
puts x
# 100
Matheus Moreira
  • 17,106
  • 3
  • 68
  • 107
  • 3
    Do you know off-hand if accessing instance variables is actually thread safe or is that simply based on my assumption that it *appears* to be? – Joseph Ravenwolfe Mar 05 '12 at 14:27
  • @AnomalousThought, see updated answer for some information about thread safety. – Matheus Moreira Mar 05 '12 at 17:24
  • @MatheusMoreira Would you mind taking a look at http://stackoverflow.com/questions/21735401/using-class-instance-variable-for-mutex-in-ruby when you get a chance? Thanks. – Peter Alfvin Feb 12 '14 at 20:49
  • I'm not actually sure what is meant by "thread safe" in this context, but based on my experience with http://stackoverflow.com/questions/21735401/using-class-instance-variable-for-mutex-in-ruby, I don't think there is anything about instance variables of any sort that makes them any more safe than any other Ruby variable. – Peter Alfvin Feb 13 '14 at 01:49
  • @PeterAlfvin, there doesn't seem to be any hard guarantee. The current GIL semantics seem to result in some degree of thread safety for everything implemented in C. This depends heavily on the implementation in use, and is likely not to hold true for anything other than MRI/YARV. – Matheus Moreira Mar 07 '14 at 11:45
  • 4
    `since you have already established instance variable access is thread safe` -- yeah, I don't think this is actually established or safe to assume. – jrochkind Jun 24 '15 at 22:24
  • 2
    > The inherent thread safety of the Ruby language depends on the implementation. This is not actually true. The assumption that class instance variables are threadsafe is also untrue. Having a GIL doesn't make your code thread safe; it ensures your code does not run concurrently. This eliminates the perils of threadsafety, but does not make the code itself threadsafe. There's no reason to write un-threadsafe code just because you're on MRI, especially if you plan to port to RBX or j/cruby some day. – sethvargo Jul 11 '16 at 19:57
18

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

Magne
  • 16,401
  • 10
  • 68
  • 88
11

Examples 2 and 3 are exactly the same. They are not thread-safety at all.

Please see the example Below.

class Foo
  def self.bar
    @bar ||= create_no
  end

  def self.create_no
    no = rand(10000)
    sleep 1
    no
  end
end

10.times.map do
  Thread.new do
    puts "bar is #{Foo.bar}"
  end
end.each(&:join)

It's result is not same. The result is same when using mutex as below.

class Foo
  @mutex = Mutex.new

  def self.bar
    @mutex.synchronize {
      @bar ||= create_no
    }
  end

  def self.create_no
    no = rand(10000)
    sleep 1
    no
  end
end

10.times.map do
  Thread.new do
    puts "bar is #{Foo.bar}"
  end
end.each(&:join)

It is run on CRuby 2.3.0.

Vincent Xie
  • 311
  • 3
  • 7
  • I'm not sure I understand. Of course the result will always be different in a thread safe manner, because each thread is able to set its own value for `@bar`. If you replace `@bar` with `@@bar` you'll always get the same result. Based on that assumption, are you saying `@@bar` is thread safe? – Magnuss Mar 28 '17 at 06:55
  • 2
    @bar is instance variable of the Foo class . It is not owned by every thread. It is shared by all the thread. – Vincent Xie May 12 '17 at 06:15
  • 1
    Actually, thread-safety would mean that the results in your topmost example _should_ be different (ie. other threads won't meddle with the instance variable), like @Magnuss indicated. So your example seems to demonstrate that example 2 and 3 in the OP's question _are_ thread-safe. – Magne Nov 21 '17 at 13:10
  • But OP's example 2 and 3 are actually _not_ thread safe, like you stated. However, your topmost example doesn't actually demonstrate that (but seem to demonstrate the opposite). Hence @Magnuss rightful confusion. – Magne Nov 21 '17 at 13:12
  • I made an answer with an example that hopefully demonstrate the non-thread-safety a bit better, using your answer as an inspiration. https://stackoverflow.com/a/47414040/380607 – Magne Nov 21 '17 at 13:17
  • If you run your topmost example code several times, then you will see it gives out the same result for bar for all threads. – Magne Nov 21 '17 at 13:54
  • Another way to see it is if you remove the `sleep 1` inside your `create_no` method, then you'll see that it actually gives out the same result in each thread, because the @bar instance variable is shared between all threads, and is thus not thread safe. The reason the `sleep 1` leads to different results (on the first execution of that example code) is probably because the second thread didn't see a @bar value, because the first thread was sleeping and hadn't set it yet, thus the second thread started creating it's own value. The same happened with the other threads, which ran in parallel. – Magne Nov 21 '17 at 13:56