Почему в Ruby `i true = i + = 1 end` не является потокобезопасным? - PullRequest
1 голос
/ 26 июня 2019

Согласно этому post , i += 1 является поточно-ориентированным в MRI Ruby, поскольку вытеснение происходит только в конце вызова функции, а не где-то между i += 1.

Повторяемый тест ниже показывает, что это правда: repeatable test

Но почему while true do i += 1 end не является потокобезопасным, как показано во втором тесте ниже, где thread1 вытесняется thread2, когда thread1 все еще выполняет while true do i += 1 end?

second test

Пожалуйста, помогите.

Ниже приведен код ссылки:

тестовый:

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

1 Ответ

6 голосов
/ 26 июня 2019

Согласно этому сообщению, i += 1 является потокобезопасным в МРТ

Не совсем. В сообщении блога говорится, что вызовы методов эффективно поддерживают потоки в 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
...