我想知道使用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!
?
答案 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
方法中切换线程,因为其中的所有操作都可以优化
以同样的方式。