||= is an extremely useful ruby operator that lots of people learn to love and use often. The problem is it doesn’t mix well with threading (shared writable variables in specific) because it’s not atomic. I’ve been noticing a lot of people making these mistakes recently, even in code which touts being thread safe (even in my own x_x).
It’s an easy mistake to make so I figured I should explain what’s wrong with it and how to avoid it. Let’s take a simple example:
class X
def lock
@mutex ||= Mutex.new
@mutex.synchronize { yield }
end
end
lock uses ||= to lazily initialize the @mutex instance var, which is normally fine, except in this case. First you need to remember/realize what ||= actually expands to:
@mutex or @mutex = Mutex.new
Ok so let’s assume two threads call the lock method at the “same time”. Here’s the scenario which demonstrates how it’s not thread safe. {{}} represents what the interpreter is currently evaluating:
# >> thread 1
{{@mutex}} or @mutex = Mutex.new
# >> thread 2 (context switch)
{{@mutex}} or @mutex = Mutex.new
# At this point, both threads evaluated @mutex to nil, so they will both go ahead with the assignment
# >> thread 1 (context switch)
@mutex or {{@mutex = Mutex.new
@mutex.synchronize}} { yield }
# thread 1 is already referencing the object stored in @mutex, ready to call synchronize on it, so if another thread changes it, it won't make a difference to thread 1
# >> thread 2 (context switch)
@mutex or {{@mutex = Mutex.new}}
# thread 2 has now "won" the race and set @mutex to the Mutex object it created. BUT both threads
# will have be acting on different Mutexes, rendering their synchronization useless.
So usually in cases where you want to use the ||= operator with a shared variable you want to actually synchronize the ||= operation with another “more global” mutex. Or in this situation just setting @mutex = Mutex.new in the initialize method would be best. Here’s an example with the global mutex.
class X
@@class_mutex = Mutex.new
def lock
@@class_mutex.synchronize { @mutex ||= Mutex.new }
@mutex.synchronize { ... }
# or
@@class_mutex.synchronize { @mutex ||= Mutex.new }.synchronize { ... }
end
end
Along the same lines a Hash with a default proc can run you into thread safety problems. Here’s an example:
h = Hash.new{|h,k| sleep Thread.current[:sleep].to_i; h[k] = Mutex.new }
t1 = Thread.new { Thread.current[:sleep] = 1; h[1] }
t2 = Thread.new { h[1] }
p t2.value # => #<Mutex:0xb7c67840>
p t1.value # => #<Mutex:0xb7c63614>
p h[1] # => #<Mutex:0xb7c63614>
The default proc will be called twice, two Mutexes will be created and you could end up in the same situation as the ||= example. The bottom line here is that when you use a default proc in a Hash or the ||= operator, you expect that the variable or hash key in question will only be set a single time. When dealing with threads this isn’t always the case.
