【问题标题】:Thread-safety for hashes in RubyRuby 中哈希的线程安全
【发布时间】:2014-05-05 15:39:08
【问题描述】:

我很好奇 Ruby 中哈希的线程安全性。从控制台运行以下命令(Ruby 2.0.0-p247):

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

返回

{0=>1000000}

这是正确的预期值。

为什么有效?在这个版本的 Ruby 中,我可以依赖哈希是线程安全的吗?

编辑:测试 100 次:

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

计数器最后仍然为 0。我尝试了多达 10K 次,并且这段代码从未遇到过单个线程安全问题。

【问题讨论】:

    标签: ruby multithreading thread-safety hashmap


    【解决方案1】:

    更准确地说,ruby 哈希中的线程安全更多地取决于运行时而不是代码。在 MRI 2.6.2 中的任何示例中,我都无法看到竞态条件。我怀疑这是在执行本机操作时不会中断 MRI 线程,并且 MRI Hash 是用 C 语言编写的。但是,在 jruby 9.2.8.0 中,我确实看到了竞争条件。

    这是我的例子:

    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
    
    

    在 jruby 下我得到这个结果:

    % jruby counter.rb
    597 != 1000
    

    在 MRI 下,我得到了这个结果,它可以运行很长时间而不显示竞争条件,然后才不得不杀死它:

    % 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
    
    

    如果我取消注释 Thread.pass if val &gt; 10 行,那么 MRI 将立即显示竞争条件。

    % ruby counter.rb
    112 != 1000
    
    % ruby counter.rb
    110 != 1000
    

    【讨论】:

      【解决方案2】:

      不,您不能依赖哈希是线程安全的,因为它们不是为线程安全而构建的,很可能是出于性能原因。为了克服标准库的这些限制,创建了提供线程安全(thread_safe)或不可变(仓鼠)数据结构的 Gems。这些将使访问数据线程安全,但除此之外,您的代码还有一个不同的问题:

      您的输出将不是确定性的;事实上,我尝试了几次你的代码,一旦我得到544988 作为结果。在您的代码中,可能会出现经典的race condition,因为涉及到单独的读取和写入步骤(即它们不是原子的)。考虑表达式h[0] ||= 0which basically translates to h[0] || h[0] = 0。现在,很容易构造一个发生竞争条件的案例:

      • 线程 1 读取 h[0] 并发现它是 nil
      • 线程 2 读取 h[0] 并发现它是 nil
      • 线程 1 设置 h[0] = 0 并递增 h[0] += 1
      • 线程 2 组 h[0] = 0 并递增 h[0] += 1
      • 生成的哈希是{0=&gt;1},尽管正确的结果是{0=&gt;2}

      如果你想确保你的数据不会被破坏,你可以锁定操作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
      

      【讨论】:

      • 另一种解决方案是不可变数据结构。 github.com/hamstergem/hamster
      • @Reactormonk 我很确定线程安全的数据结构不会处理竞争条件的问题。通过引用 thread_safe gem 犯了同样的错误 ;-) 这些 gem 只提供对数据结构的线程安全访问,但这里的问题在于与数据结构无关的单独的读/写步骤。
      • 谢谢!我知道这种类型的代码可能出现的那些 gem 和线程安全问题。我用一个示例实验编辑了我的问题,但我一次都无法用代码重现线程安全问题。我很好奇为什么这段代码没有破坏,如果这只是一个例外,或者它总是会这样工作。
      • 我认为它只是偶然发挥作用,机会很大,但你不能指望它总是这样。
      • 这是另一个线程,解释了为什么即使 +=||= 本身也不是线程安全的:stackoverflow.com/questions/15184338/…
      猜你喜欢
      • 1970-01-01
      • 2011-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-29
      • 1970-01-01
      • 2016-05-05
      相关资源
      最近更新 更多