Search code examples
rubymultithreadingthread-safetyhashmap

Thread-safety for hashes in Ruby


I'm curious about thread safety for hashes in Ruby. Running the following from the console (Ruby 2.0.0-p247):

h = {}
10.times { Thread.start { 100000.times {h[0] ||= 0; h[0] += 1;} } }

returns

{0=>1000000}

which is the correct expected value.

Why does it work? Can I rely on hashes being thread-safe with this version of Ruby?

Edit: Testing 100 times:

counter = 0
100.times do
  h={}
  threads = Array.new(10) { Thread.new { 10000.times { h[0] ||= 0; h[0] += 1 } } }
  threads.map { |thread| thread.join }
  counter += 1 if h[0] != 100000
end
puts counter

Counter is still 0 at the end. I tried up to 10K times and never had a single thread-safety issue with this code.


Solution

  • No, you cannot rely on Hashes being thread safe, because they aren't built to be thread safe, most probably for performance reasons. In order to overcome these limitations of the standard library, Gems have been created which provide thread safe (concurrent-ruby) or immutable (hamster) data structures. These will make accessing the data thread safe, but your code has a different problem in addition to that:

    Your output will not be deterministic; in fact, I tried you code a few times and once I got 544988 as result. In your code, a classical race condition can occur because there are separate reading and writing steps involved (i.e. they are not atomic). Consider the expression h[0] ||= 0, which basically translates to h[0] || h[0] = 0. Now, it is easy to construct a case where a race condition occurs:

    • thread 1 reads h[0] and finds it is nil
    • thread 2 reads h[0] and finds it is nil
    • thread 1 sets h[0] = 0 and increments h[0] += 1
    • thread 2 sets h[0] = 0 and increments h[0] += 1
    • the resulting hash is {0=>1} although the correct result would be {0=>2}

    If you want to make sure that your data will not be corrupted, you can lock the operation with a mutex:

    require 'thread'
    semaphore = Mutex.new
    
    h = {}
    
    10.times do
      Thread.start do
        semaphore.synchronize do
          100000.times {h[0] ||= 0; h[0] += 1;}
        end
      end
    end
    

    NOTE: An earlier version of this answer mentioned the 'thread_safe' gem. 'thread_safe' is deprecated since Feb 2017, becoming part of the 'concurrent-ruby' gem. Use that one instead.