在 Ruby 中,为什么 `while true do i += 1 end` 不是线程安全的?

In Ruby, why `while true do i += 1 end` is not thread safe?

根据这个 posti += 1 在 MRI Ruby 中是线程安全的,因为抢占只发生在函数调用结束时,而不是 i += 1 之间的某处。

下面的可重复测试表明这是真的:

但是为什么 while true do i += 1 end 不是线程安全的,如下面的第二个测试所示,当线程 1 仍在执行时,线程 1 被线程 2 抢占 while true do i += 1 end ?

请帮忙。

代码参考如下:

测试一:

100.times do
  i = 0
  1000.times.map do
    Thread.new {1000.times {i += 1}}
  end.each(&:join)
  puts i
end

测试二:

t1 = Thread.new do
  puts "#{Time.new} t1 running"
  i = 0
  while true do i += 1 end
end

sleep 4

t2 = Thread.new do
  puts "#{Time.new} t2 running"
end

t1.join
t2.join

According to this post, i += 1 is thread safe in MRI

不完全是。博客 post 声明 方法调用 在 MRI 中有效 thread-safe。

缩写赋值 i += 1 是语法糖:

i = i + 1

所以我们有一个赋值 i = ... 一个方法调用 i + 1。根据博客post,后者是thread-safe。但它也表示 thread-switch 可以在返回方法结果之前发生,即在结果为 re-assigned 到 i:

之前
i = i + 1
#  ^
# here

不幸的是,要从 Ruby 内部进行演示并不容易。

然而,我们可以挂接到 Integer#+ 并随机要求线程调度程序将控制权传递给另一个线程:

module Mayhem
  def +(other)
    Thread.pass if rand < 0.5
    super
  end
end

如果 MRI 确保整个 i += 1 语句的 thread-safety,则上述内容不应有任何影响。但确实如此:

Integer.prepend(Mayhem)

10.times do
  i = 0
  Array.new(10) { Thread.new { i += 1 } }.each(&:join)
  puts i
end

输出:

5
7
6
4
4
8
4
5
6
7

如果您需要 thread-safe 代码,请不要依赖实现细节(这些细节可能会更改)。在上面的示例中,您可以将敏感部分包装在 Mutex#synchronize 调用中:

Integer.prepend(Mayhem)

m = Mutex.new

10.times do
  i = 0
  Array.new(10) { Thread.new { m.synchronize { i += 1 } } }.each(&:join)
  puts i
end

输出:

10
10
10
10
10
10
10
10
10
10