Chengsheng Wen
Chengsheng Wen

Reputation: 55

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

Upvotes: 2

Views: 221

Answers (1)

Stefan
Stefan

Reputation: 114237

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

Upvotes: 7

Related Questions