2

According to this post, i += 1 is thread safe in MRI Ruby because the preemption only happens at the end of function call, not somewhere between i += 1.

A repeatable test below shows that this is true: repeatable test

But why while true do i += 1 end is not thread safe, as shown by the second test below where thread1 is preempted by thread2 when thread1 is still executing while true do i += 1 end ?

second test

Please help.

Below are the code reference:

test one:

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

test two:

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
  • The first test does not prove anything, save for the that in 100 times there was no preemption happened. – Aleksei Matiushkin Jun 26 '19 at 07:55
  • How does the second example show that `i += 1` isn't thread-safe? – Stefan Jun 26 '19 at 08:05
  • @Stefan Source code appended. – Chengsheng Wen Jun 26 '19 at 08:07
  • @AlekseiMatiushkin I have run the test one repeatedly and all show the same result. Any more, If I run the same test using jruby, the result is variable. – Chengsheng Wen Jun 26 '19 at 08:12
  • 1
    One cannot prove anything by showing any huge number of confirmations. Proves work the other way round. – Aleksei Matiushkin Jun 26 '19 at 08:13
  • @Stefan Second example shows that `while true do i += 1 end` is not thread-safe in that while thread1 hasn't finished its execution, thread2 got its CPU time to execute some code. If thread1 is not preemptive(thread-safe), thread2 should not have a chance to execute. But it does have. – Chengsheng Wen Jun 26 '19 at 08:24
  • @AlekseiMatiushkin I agree. The test is a confirmation, not the proof. The proof is the implementation of MRI, which is detailed in this [post](https://www.jstorimer.com/blogs/workingwithcode/8100871-nobody-understands-the-gil-part-2-implementation). – Chengsheng Wen Jun 26 '19 at 08:28
  • Okay, given that `i += 1` is thread-safe (I haven't checked), what makes you think that the whole statement `while true do i += 1 end` must be thread-safe (or "atomic"), too? There could be a thread switch right before `i += 1` or afterwards. – Stefan Jun 26 '19 at 08:29
  • @Stefan Yes. In other words, If `i += 1` is executed atomically, `while true do i += 1 end` should be executed atomically too. – Chengsheng Wen Jun 26 '19 at 08:34
  • 1
    @ChengshengWen the blog post states that in MRI the interrupt flag is checked before returning from a method and concludes that method invocations are therefore atomic. In other words: the atomicity ends when returning from a method. Which means your `while` isn't covered. – Stefan Jun 26 '19 at 08:46
  • @Stefan You mean `while` executes while body by using some function call? If it does, the answer is obvious. But is there any supporting documentation? I google it but fail to find anything about how `while` statement works. – Chengsheng Wen Jun 26 '19 at 09:06
  • You talk about two different things in your question. In your title, you ask about Ruby, in your text, you ask about MRI. *Those are two completely different things!* Which of the two are you asking about? Also, note that MRI is no longer being developed, maintained, or supported, and hasn't been for years. The currently maintained Ruby implementations are, to my knowledge, Rubinius, TruffleRuby, Opal, JRuby, MRuby, RubyMotion / DragonRuby, YARV, RubyOMR(???), and MagLev(???), and maybe for some *really* generous definition of "maintained" IronRuby. – Jörg W Mittag Jun 29 '19 at 06:08

1 Answers1

7

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

Not quite. The blog post states that method invocations are effectively thread-safe in MRI.

The abbreviated assignment i += 1 is syntactic sugar for:

i = i + 1

So we have an assignment i = ... and a method call i + 1. According to the blog post, the latter is thread-safe. But it also says that a thread-switch can occur right before returning the method's result, i.e. before the result is re-assigned to i:

i = i + 1
#  ^
# here

Unfortunately this isn't easy do demonstrate from within Ruby.

We can however hook into Integer#+ and randomly ask the thread scheduler to pass control to another thread:

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

If MRI ensures thread-safety for the whole i += 1 statement, the above shouldn't have any effect. But it does:

Integer.prepend(Mayhem)

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

Output:

5
7
6
4
4
8
4
5
6
7

If you want thread-safe code, don't rely on implementation details (those can change). In the above example, you could wrap the sensitive part in a Mutex#synchronize call:

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

Output:

10
10
10
10
10
10
10
10
10
10
Stefan
  • 109,145
  • 14
  • 143
  • 218
  • The fact that `i + 1` is a method call, i.e. it's equal to i+(1), helps me a lot. It's very different from compiled language such as C. – Chengsheng Wen Jun 26 '19 at 10:41
  • @ChengshengWen to be fair, the blog post says _"methods implemented in C are atomic"_ so it doesn't apply to our overridden method. But the example clearly shows that MRI doesn't try to keep larger statements like `i = ...` atomically. (let alone loops with conditions) – Stefan Jun 26 '19 at 10:53
  • I know. Use of `rand` inside overridden may cause the checking of interrupt flag, which makes `i = i + 1` preemptive as a whole unexpectedly. – Chengsheng Wen Jun 27 '19 at 01:28