用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
方法中切换线程,因为其中的所有操作都可以以相同的方式进行优化。