rubymultithreadingmri

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


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

Solution

  • 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