2

根据这篇文章i += 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
4

1 回答 1

7

根据这篇文章,i += 1在 MRI 中是线程安全的

不完全的。该博客文章指出,方法调用在 MRI 中是有效的线程安全的。

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

i = i + 1

所以我们有一个赋值i = ... 一个方法调用i + 1。根据博客文章,后者是线程安全的。但它也说线程切换可以在返回方法的结果之前发生,即在结果重新分配给之前i

i = i + 1
#  ^
# here

不幸的是,在 Ruby 中进行演示并不容易。

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

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

如果 MRI 确保整个i += 1语句的线程安全,则上述内容不应该有任何影响。但它确实:

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

如果你想要线程安全的代码,不要依赖实现细节(那些可以改变)。在上面的示例中,您可以将敏感部分包装在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
于 2019-06-26T09:37:56.120 回答