用MRI产生竞争条件

ran*_*oke 6 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
Run Code Online (Sandbox Code Playgroud)

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

Nei*_*ter 4

您的示例确实(几乎立即)在 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
Run Code Online (Sandbox Code Playgroud)

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以避免这种情况仍然不会触发额外的竞争条件。

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

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

当我研究这个粗略的答案时,我发现了一个有趣的链接:没有人理解 GIL(第 2 部分链接,与这个问题更相关)


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

COUNT   = 500000
Run Code Online (Sandbox Code Playgroud)

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

def inc
  a  = $x + 1
  b = 0
  b += 1
  $x =  a
end
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

我的解释是,Ruby 解析器已经过优化b += 1,消除了方法分派的一些开销。优化的步骤之一可能包括检查是否可能切换到等待线程。

如果是这样的话,那么问题中的代码可能永远没有机会在方法内切换线程inc,因为它内部的所有操作都可以以相同的方式进行优化。