用MRI产生竞争条件

我想知道使用MRI ruby​​(2.0.0)和一些全局变量来制作竞争条件是否容易,但事实certificate这并不容易。 看起来它应该在某些时候失败,但它没有,我已经运行了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 

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

您的示例(几乎立即)在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; 500.times { b *= 100 } ,则在上面的代码中没有检测到竞争条件。 但是在b = a; 2500.times { b *= 100 }情况下更进一步b = a; 2500.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实现中发生的primefaces过程(例如,在Fixnum乘法或除法期间)期间不切换线程之间的上下文。 这意味着线程上下文切换的唯一机会是所有方法都是在没有I / O等待的情况下调用Ruby内部,而是在每行代码之间“中间”。 在最初的例子中,只有4个这样的转瞬即逝的机会,似乎在事物的方案中,这对于MRI 1.9.3+来说根本不是很明显(事实上,见下面的更新,这些机会可能已被删除)通过Ruby)

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

虽然我正在研究这个粗略的答案,但我发现了一个有趣的链接: 没有人理解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方法中切换线程,因为其中的所有操作都可以以相同的方式进行优化。