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:
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
?
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
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