使用MRI生成比赛条件 [英] Generating a race condition with MRI

查看:75
本文介绍了使用MRI生成比赛条件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想知道使用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.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>

在研究这个大致答案时,我发现了一个有趣的链接:没人理解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方法内切换线程,因为可以对其中的所有操作进行优化. 以同样的方式.

I was wondering whether it's easy to make a race condition using MRI ruby(2.0.0) and some global variables, but as it turns out it's not that easy. It looks like it should fail at some point, but it doesn't and I've been running it for 10 minutes. This is the code I've been trying to achieve it:

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

Why am I not able to generate (or detect) the expected race condition, and get the output woo hoo! in Ruby MRI 2.0.0?

解决方案

Your example does (almost instantly) work in 1.8.7.

The following variation does the trick for 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

The sleep command is a strong hint to the interpreter that it can schedule another thread, so this is not a huge surprise.

Note if you replace the sleep with something that takes just as long or longer, e.g. b = a; 500.times { b *= 100 }, then there is no race condition detected in the above code. But take it further with b = a; 2500.times { b *= 100 }, or increase COUNT from 50 to 500, and the race condition is more reliably triggered.

The thread scheduling in Ruby 1.9.3 onwards (of course including 2.0.0) appears to be assigning CPU time in larger chunks than in 1.8.7. Opportunities to switch threads can be low in simple code, unless some kind of I/O waiting is involved.

It is even possible that the threads in the OP, each of which is performing just a few thousand calculations, are in essence occurring in series - although increasing the COUNT global to avoid this still does not trigger additional race conditions.

Generally MRI Ruby does not switch context between threads during atomic processes (e.g. during a Fixnum multiply or division) that occur within its C implementation. This means that the only opportunities for a thread context switch where all methods are calls to Ruby internals without I/O waiting, are "in-between" each line of code. In the original example, there are only 4 such fleeting opportunities, and it seems that in the scheme of things that this is not very much at all for MRI 1.9.3+ (in fact, see update below, these opportunities probably have been removed by Ruby)

When I/O waits or sleep are involved, it actually gets more complex, as Ruby MRI (1.9+) will allow a little bit of true parallel processing on multi-core CPUs. Although this is not the direct cause of race conditions with threads, it is more likely to result in them, as Ruby will usually make a thread context switch at the same time to take advantage of the parallelism.

Whilst I was researching this rough answer, I found an interesting link: Nobody understands the GIL (part 2 linked, as more relevant to this question)


Update: I suspect that the interpretter is optimising away some potential thread-switching points in the Ruby source. Starting with my sleep version of the code, and setting:

COUNT   = 500000

the following variation of inc does not seem to have a race condition affecting $x:

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

However, these minor changes both trigger a race condition:

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

My interpretation is that the Ruby parser has optimised b += 1 to remove some of the overhead of method despatch. One of the optimised-away steps is likely to include the check for a possible switch to a waiting thread.

If that is the case, then the code in the question may never have the opportunity to switch threads within the inc method, because all the operations inside it can be optimised in the same way.

这篇关于使用MRI生成比赛条件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆