4

Note: The code summary shown below is not a distillation of the code that I had the problem with. I've left this original summary here since someone else already answered, but the actual code is shown in the answer I've provided below.

I haven't been able to isolate this to a small failing test case, but I'm getting a failure with the following general construct:

class Foo

  @mutex = Mutex.new

  ....

  def self.bar
    @mutex.synchronize { ... }
  end

end

If I create multiple threads invoking Foo.bar, sometimes @mutex will evaluate to nil in bar. If I use a constant (e.g. MUTEX) instead of an instance variable, I don't have this problem.

I don't know if it's significant, but I'm running on JRuby on a multi-core machine.

I'd appreciate any explanation or help in how to isolate the problem.

Update: I believe this is related to autoloading. With Rails, I was able to reproduce a similar problem with the following contents of foo.rb in one of the directories Rails autoloads from:

class Foo
  @mutex = Mutex.new
  def self.bar
    @mutex.synchronize {}
  end
end

When I then execute the following in the Rails console:

1.upto(4).map { Thread.new { Foo.bar }}.map(&:join)

I get the following error:

RuntimeError: Circular dependency detected while autoloading constant Foo
    from /Users/palfvin/.rvm/gems/jruby-1.7.10@javlats/gems/activesupport-4.0.1/lib/active_support/dependencies.rb:461:in `load_missing_constant'
    from /Users/palfvin/.rvm/gems/jruby-1.7.10@javlats/gems/activesupport-4.0.1/lib/active_support/dependencies.rb:184:in `const_missing'
    from (irb):1:in `evaluate'

and this behavior is the same in CRuby (MRI Ruby).

Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106

2 Answers2

8

Does it happen with a class variable? @@mutex. There might be a race condition with making new class-instances between threads and the new copy of @mutex isn't ready yet. Constants and class variables however, are shared between copies of the class and subclasses. Also, what if you put the @mutex init code in a memoized method such as:

def self.mutex
  @mutex ||= Mutex.new
end
DiegoSalazar
  • 13,361
  • 2
  • 38
  • 55
  • 1
    I tried your two alternatives and the problem didn't occur with either of them, but your phrase "copy of @mutex" is worrisome, because unless all threads share the same mutex, the mutex isn't doing anything. :-) – Peter Alfvin Feb 12 '14 at 18:19
  • I just mean it has to reinstantiate it when the new thread inits since its a class **instance** variable. Whereas the constant or class variable approach is shared. And the memoized method approach checks for a nil first and then inits. – DiegoSalazar Feb 12 '14 at 18:21
  • I don't understand why it would need to be reinstantiated. Are you saying that each thread has it's own instance of the class? – Peter Alfvin Feb 12 '14 at 18:26
  • Just ran a test under ruby 2.0, correction, they all have the same instance, but i can't recreate your problem. I ran this test, they don't come out nil and they all have the same object_ids: https://gist.github.com/DiegoSalazar/8961861 – DiegoSalazar Feb 12 '14 at 18:40
0

While autoloading is indeed not thread safe in Rails like it is in Ruby 1.9 (per Is autoload thread-safe in Ruby 1.9?), the problem I encountered was not due to that problem and the code I had was not an instance of the code I showed above, but rather an instance of the following:

class Foo
  @mutex = Mutex.new
  def self.bar
     @mutex.synchronize { }
  end
end

class Foobar < Foo ; end

Foobar.bar

The problem is that when executing method from a superclass, the value of self remains unchanged, so the value of @mutex within Foo.bar is interpreted in the context of the Foobar object, not the value of the Foo object.

This problem can be avoided by using a class variable (e.g. @@mutex) for the mutex.

Community
  • 1
  • 1
Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106