问题 为什么Ruby中没有竞争条件


我正在尝试多线程示例。我正在尝试使用以下代码生成竞争条件。但我总是得到相同(正确)的输出。

class Counter
  attr_reader :count
  def initialize
    @count = 0
  end
  def increment
    @count += 1
  end
  def decrement
    @count -= 1
  end
end
c = Counter.new
t1 = Thread.start { 100_0000.times { c.increment } }
t2 = Thread.start { 100_0000.times { c.increment } }
t1.join
t2.join
p c.count #200_0000

我能够在每个线程中使用少得多的迭代次数来观察Java中的竞争条件。是不是我没有运行足够多次来产生竞争条件,或者 +/- Ruby中的线程安全吗?我使用的是ruby 2.0.0p247


5616
2017-11-14 04:26


起源

我希望这是因为你总是递增计数器。 c的值在两个线程的相同位置,并且可以在那里递增。一个有趣的测试是分别为每个线程计时,然后将它们计时。这是你期望的吗?只是抛出各种想法 - joncodo
由于对例如一些优化,简单的数学运算情况更复杂。 += 因此,永远不会进行允许GIL切换线程的调用。看看我对这个问题的回答: stackoverflow.com/questions/18574254/... - Neil Slater
你的代码也 不 显示Ruby 1.8.7(MRI和REE),JRuby和Rubinius的竞争条件 - 我刚在我的机器上演示了这一点。您接受的答案特定于MRI Ruby 1.9+(最受欢迎)。如果你想编写真正的线程安全的Ruby,你应该把它当作你的Java例子(你熟悉Java线程) 将 帮助),而不是对GIL作出任何假设。 - Neil Slater
JRuby(9.1.13)怎么样?我认为它应该提供真正的线程(而不是使用GIL),但我得到的结果与MRI 2.4.1相同(即一致计数,除非我根据下面的答案模拟比赛)。 - labyrinth
我之前的评论更新: 用脚射击自己。我使用shebang运行代码,所以实际上并没有使用JRuby。一旦我使用JRuby切换到它与rvm并做 ruby ./counter.rb 它按预期工作。我会删除评论,但时间到了。 - labyrinth


答案:


这是因为由于GIL,MRI Ruby线程并不真正并行(参见 这里),在CPU级别,它们一次执行一个。

因此,线程中的每个命令一次执行一个 @count 在每个线程中始终正确更新。

可以通过添加另一个变量来模拟竞争条件:

class Counter
    attr_accessor :count, :tmp

    def initialize
        @count = 0
        @tmp = 0
    end

    def increment
        @count += 1
    end


end

c = Counter.new

t1 = Thread.start { 1000000.times { c.increment; c.tmp += 1 if c.count.even?; } }
t2 = Thread.start { 1000000.times { c.increment; c.tmp += 1 if c.count.even?; } }

t1.join
t2.join

p c.count #200_0000
p c.tmp # not 100_000, different every time

给出了一个很好的竞争条件的例子 这里,为了完整性复制如下

class Sheep
  def initialize
    @shorn = false
  end

  def shorn?
    @shorn
  end

  def shear!
    puts "shearing..."
    @shorn = true
  end
end


sheep = Sheep.new

5.times.map do
  Thread.new do
    unless sheep.shorn?
      sheep.shear!
    end
  end
end.each(&:join)

这是我在MRI 2.0上多次运行时看到的结果。

$ ruby​​ check_then_set.rb =>剪毛......

$ ruby​​ check_then_set.rb =>剪切...剪切......

$ ruby​​ check_then_set.rb =>剪毛......   剪切...

有时同样的羊被剪了两次!


12
2017-11-14 05:03



如果我理解正确,在增量方法中添加更多语句也应该产生竞争条件,对吧?我只是尝试过,但我无法生产它。 - goyalankit
只要为两个线程更新所访问的变量,添加更多语句就不一定会产生竞争条件。 - tihom
谢谢...但我想说的是它可能产生它,对吧? - goyalankit
在不更改变量访问方式的情况下,不会通过向原始代码添加额外语句来生成竞争条件。问题不在于每个线程所花费的时间。 - tihom
一个非常简单的补充:自1.9.3版以来的Ruby线程通常是100ms的时间片。这意味着100,000次简单代码的迭代很少会引发竞争条件。线程甚至没有并发运行。 - Neil Slater


Ruby有一个 全球翻译锁。发生的一切 在Ruby中 基本上是同步的。所以你引用的问题是你在Java等低级语言中遇到的问题,其中两个线程可能会读取相同的值并相互冲突 += - 不是问题。

在哪里 Thread class派上用场的是你编写的代码,它可以将事物带到Ruby之外,例如,使用文件或网络I / O,进行系统调用,或通过绑定与C库连接。


1
2017-11-14 04:57



仅适用于Ruby MRI 1.9+。 - Neil Slater
@NeilSlater:你是说Ruby 2.0摆脱了GIL吗?我没有看到任何地方(虽然不是说你错了 - 我本可以错过它)。参考将不胜感激。 - Dan Tao
不,因此 +,但我可能会更清楚。仅适用于Ruby MRI 1.9.x和2.0.0。我在我安装的所有版本中测试了OP代码。 MRI 1.9.3和2.0.0按照问题表现,但其他人没有。事实上,大多数主要的Ruby版本(与使用中的大部分Ruby相反)都不像OP经历的那样。 - Neil Slater


这将归功于Ruby 2.0 全球口译员锁

简而言之,由于Ruby解释器的底层实现,任何非IO的操作(例如文件读/写)都将同步发生。

看到:


1
2017-11-14 05:01



我建议也加上优秀的 jstorimer.com/blogs/workingwithcode/... - Neil Slater


在Ruby中看到竞争条件的非常简单的方法:

i = 0
2.times do
  Thread.new do
    30_000_000.times do # this should take more than 100ms
      a = i + 1
      i = a
    end
  end
end
puts i # the value will always be different

没有竞争条件的例子:

i = 0
2.times do
  Thread.new do
    10_000.times do # this should take less than 100ms
      a = i + 1
      i = a
    end
  end
end
puts i # 20000, always!

i = 0
2.times do
  Thread.new do
    30_000_000.times do # it doesn't matter how much time it takes
      i += 1
    end
  end
end
puts i # 60000000, always!

1
2018-03-25 14:14



如果我在Ruby 1.9.3上运行它,而不是Ruby 2.4.1,我可以获得竞争条件。如果我使用JRuby 9.13(并且它比任何一个都慢,奇怪的话),即使是简单的单一赋值,我也会得到非确定性。所以我学到的教训是,如果你要对线程做任何事情并关心预期的行为和性能,你最好真正了解你的Ruby版本以及它如何处理线程。 - labyrinth


我也试图理解这一点,并在此代码中获得c.count的不同结果(从上面复制)。例如,我得到c.coint = 1,573,313或1,493,791等。看看代码,似乎c.count每次应该是2,000,000!

class Counter
    attr_accessor :count, :tmp

    def initialize
        @count = 0
        @tmp = 0
    end

    def increment
        @count += 1
    end
end

c = Counter.new

t1 = Thread.start { 1_000_000.times { c.increment; c.tmp += 1 if c.count.even?; } }
t2 = Thread.start { 1_000_000.times { c.increment; c.tmp += 1 if c.count.even?; } }

t1.join
t2.join

p c.count # Varies e.g. 1,573,313 or 1,493,791 etc
p c.tmp # Also varies: 882,928 etc.

0
2018-04-13 01:00





对于Java,您只能在异步线程中获取竞争条件。找到您需要的确切解决方案可能很有用。


-1
2017-11-14 05:24