It is more accurate to say thread safety in ruby hashes depends more upon the runtime than on the code. I wasn't able to witness a race condition in any of the examples in MRI 2.6.2. I suspect this to be that MRI threads won't be interrupted when native operations are being executed and MRI Hash is native written in C. However, in jruby 9.2.8.0 I did see the race condition.
Here is my example:
loops = 100
round = 0
while true do
round += 1
h={}
work = lambda do
h[0] = 0 if h[0].nil?
val = h[0]
val += 1
# Calling thread pass in MRI will absolutely exhibit the classic race
# condition described in https://en.wikipedia.org/wiki/Race_condition .
# Otherwise MRI doesn't exhibit the race condition as it won't interrupt the
# small amount of work taking place in this lambda.
#
# In jRuby the race condition will be exhibited quickly.
# Thread.pass if val > 10
h[0] = val
end
threads = Array.new(10) { Thread.new { loops.times { work.call } } }
threads.map { |thread| thread.join }
expected = loops * threads.size
if h[0] != expected
puts "#{h[0]} != #{expected}"
break
end
puts "round #{round}" if round % 10000 == 0
end
Under jruby I get this result:
% jruby counter.rb
597 != 1000
Under MRI I get this result which will run without exhibiting the race condition for a long time before having to kill it:
% ruby counter.rb
round 10000
round 20000
round 30000
round 40000
round 50000
round 60000
...
round (very large number)
^CTraceback (most recent call last):
3: from counter.rb:25:in `<main>'
2: from counter.rb:25:in `map'
1: from counter.rb:25:in `block in <main>'
counter.rb:25:in `join': Interrupt
If I uncomment the Thread.pass if val > 10
line then MRI will exhibit the race condition immediately.
% ruby counter.rb
112 != 1000
% ruby counter.rb
110 != 1000