用MRI产生竞争条件

时间:2013-09-02 13:17:28

标签: ruby multithreading race-condition

我想知道使用MRI ruby​​(2.0.0)和一些全局变量来制作竞争条件是否容易,但事实证明这并不容易。看起来它应该在某些时候失败,但它没有,我已经运行了10分钟。这是我一直试图实现的代码:

def inc(*)
  a  = $x
  a +=  1
  a *= 3000
  a /= 3000
  $x =  a
end

THREADS = 10
COUNT   = 5000

loop do
  $x = 1
  THREADS.times.map do Thread.new { COUNT.times(&method(:inc)) } end.each(&:join)

  break puts "woo hoo!" if $x != THREADS * COUNT + 1
end

puts $x

为什么我无法生成(或检测)预期的竞争条件,并在Ruby MRI 2.0.0中获得输出woo hoo!

1 个答案:

答案 0 :(得分:3)

您的示例 (几乎立即)在1.8.7中工作。

以下变体为1.9.3 +提供了技巧:

def inc
  a  = $x + 1
  # Just one microsecond
  sleep 0.000001
  $x =  a
end

THREADS = 10
COUNT   = 50

loop do
  $x = 1
  THREADS.times.map { Thread.new { COUNT.times { inc } } }.each(&:join)
  break puts "woo hoo!" if $x != THREADS * COUNT + 1
  puts "No problem this time."
end

puts $x

sleep命令强烈暗示解释器可以安排另一个线程,所以这不是一个大惊喜。

请注意,如果您将sleep替换为需要更长或更长时间的内容,例如b = a; 500.times { b *= 100 },然后在上面的代码中没有检测到竞争条件。但是可以使用b = a; 2500.times { b *= 100 }进一步使用,或者将COUNT从50增加到500,并且可以更可靠地触发竞争条件。

Ruby 1.9.3以后的线程调度(当然包括2.0.0)似乎是以比1.8.7更大的块分配CPU时间。除非涉及某种类型的I / O等待,否则在简单代码中切换线程的机会可能很低。

甚至可能OP中的线程(每个线程只执行几千个计算)本质上是串联发生的 - 尽管增加COUNT全局以避免这种情况仍然不会引发额外的竞争条件。

通常,MRI Ruby在其C实现中发生的原子过程(例如,在Fixnum乘法或除法期间)期间不切换线程之间的上下文。这意味着线程上下文切换的唯一机会是所有方法都是在没有I / O等待的情况下调用Ruby内部,而是在每行代码之间“中间”。在最初的例子中,只有4个这样的转瞬即逝的机会,似乎在事物的方案中,这对于MRI 1.9.3+来说根本不是很明显(事实上,见下面的更新,这些机会可能已被删除)通过Ruby)

当涉及I / O等待或sleep时,它实际上变得更加复杂,因为Ruby MRI(1.9+)将允许在多核CPU上进行一些真正的并行处理。虽然这不是带有线程的竞争条件的直接原因,但它更有可能导致它们,因为Ruby通常会同时进行线程上下文切换以利用并行性。 / p>

虽然我正在研究这个粗略的答案,但我找到了一个有趣的链接:Nobody understands the GIL(第2部分链接,与此问题更相关)


更新:我怀疑解释器正在优化一些潜在的线程切换点 在Ruby源代码中。从我的sleep版代码开始,然后设置:

COUNT   = 500000

inc的以下变体似乎没有影响$x的竞争条件:

def inc
  a  = $x + 1
  b = 0
  b += 1
  $x =  a
end

但是,这些微小的变化都会触发竞争条件:

def inc
  a  = $x + 1
  b = 0
  b = b.send( :+, 1 )
  $x =  a
end

def inc
  a  = $x + 1
  b = 0
  b += '1'.to_i
  $x =  a
end

我的解释是Ruby解析器已经优化b += 1以删除一些 方法发送的开销。其中一个优化的步骤可能包括 检查可能切换到等待线程。

如果是这种情况,那么问题中的代码可能永远有机会在inc方法中切换线程,因为其中的所有操作都可以优化 以同样的方式。