-
-
Save rosenfeld/8056742 to your computer and use it in GitHub Desktop.
MRI 2.0.0p353: | |
$ ruby test.rb | |
1 | |
1 | |
102 | |
102 | |
JRuby 1.7.9: | |
$ ruby test.rb | |
19 | |
119 | |
120 | |
120 |
require 'thread' | |
class A | |
attr_reader :a | |
@@lock = Mutex.new | |
@@incrementer = 0 | |
def a | |
sleep 1 | |
@a ||= @@lock.synchronize{@@incrementer += 1} | |
end | |
def incrementer | |
@@incrementer | |
end | |
end | |
a = A.new | |
100.times.map do | |
Thread.start{a.a} | |
end.each &:join | |
100.times.map do | |
Thread.start{A.new.a} | |
end.each &:join | |
p a.a, a.incrementer, A.new.a, a.incrementer |
Exactly :-)
I suspect you're seeing a side effect of the way that ||=
, specifically, is expanded by Ruby. That is, a ||= 42
is equivalent to a || a = 42
and not a = a || 42
. So, in this case, the critical line is expanded to:
@a || @a = @@lock.synchronize{@@incrementer += 1}
...and, IIRC, since the "or" and the assignment are individual expressions, the GVL will lock around them independently. So, starting multiple threads simultaneously, it's possible that more than one will get through @a ||
before the first one gets to @a = ...
, so multiple threads will execute the rhs of the "or".
@jballanc that part I understand although I suspected MRI threads wouldn't be interrupted in that scenario. I'm mostly confused by the output in MRI as I'd expect it to be similar to JRuby 's.
Array#join ignores the given block.
Your code doesn't wait any threads.
Hi @nobu, sorry I don't understand why you think so. Would you mind to further explain why you think the code won't wait for all threads to join?
Because 100.times.map do end
returns an Array
.
And Array#join
ignores the block. I really have to fix that...
Replace join &:join
by each &:join
and you get 1, 101, 102, 102
Hi @nobu, @marcandre. Yes, sorry, I intended to write "each &:join", not "join &:join". You're right!
@headius, I always see the same result with the fixed code in MRI (1, 101, 102, 102). Could you please try it fixed and see if you still get those other results? I still believe @A ||= xxx will be mostly atomic in MRI. At least 99.999% of the times... That's why so many bugs only seem to manifest in JRuby and that's why I proposed JRuby to perform the assign atomically automatically.
Anyone replying to me could please add "@rosenfeld" in the comment so that I get notified?
@rosenfeld Now the problem is that the RHS of the ||= is not substantial enough for threads to context switch.
MRI does not run threads concurrently. Each thread gets a slice of time to execute before it gets put back into the queue and another thread gets a chance to run. In this case, the #a method's logic runs easily within that timeslice.
If we replace the #a method with one that forces a thread context switch, MRI fails to do the assigns atomically just like JRuby:
def a
sleep 1
@a ||= begin; Thread.pass; new_a = @@lock.synchronize{@@incrementer += 1}; Thread.pass; new_a; end
end
This gives me a "bad" result immediately:
$ rvm ruby-2.1 do ruby blah.rb
2
102
103
103
This may seem artificial, but if the RHS had a bit more work to do, like calculating an expensive value or making a database connection, it would be very easy for MRI to schedule another thread while that code is still running.
Trust me...there's absolutely nothing guaranteeing that ||= runs atomically on MRI.
Yeah that's really confusing. In the same line, a.incrementer produces wildly different values.