[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

Re: Synchronized attr_accessor

MenTaLguY

6/11/2007 6:46:00 PM

On Tue, 12 Jun 2007 03:13:39 +0900, "Nasir Khan" <rubylearner@gmail.com> wrote:
> @@__ms_c_lock.synchronize { @__ms_lock = Mutex.new unless
> @__ms_lock
> } unless @__ms_lock
>
> What is the race condition...?

It's known as the "double-checked locking" antipattern. Generally, anything like:

unless @obj
@lock.synchronize do
unless @obj
@obj = Whatever.new
end
end
end
@obj.some_method

is wrong. This is because (when dealing with real multi-threading -- not a major issue in Ruby yet, but it soon will be!) we depend on locks not only for preserving the order of operations, but ensuring that each thread has a consistent view of memory. For instance, when reading the instance variable @obj outside the protection of the lock, it is possible that a thread could @obj being non-null, but not seeing the effects of initializing the object until much later!

If you're interested in learning more about the details of the problem, read here:

http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLo...

The article deals with Java, but in practice the problems apply to pretty much any contemporary language with native threads (and certainly to JRuby!).

Anwyay, the correct approach is something like this:

obj = @lock.synchronize { @obj ||= Whatever.new }
obj.some_method

Or even the slightly more verbose:

obj = @lock.synchronize do
@obj = Whatever.new unless @obj
@obj
end
obj.some_method

Note that all assignments to @obj and all reads of @obj happen within the protection of the lock.

> And why did you change it to have two identical unless's one after the
> other in the second snippet?

Yes, the two unlesses are indeed redundant in that case, and failed to fix the problem generally if there were reads of the instance variable outside the lock.

-mental


3 Answers

MenTaLguY

6/11/2007 6:51:00 PM

0

On Tue, 12 Jun 2007 03:45:36 +0900, MenTaLguY <mental@rydia.net> wrote:
> For instance, when reading the instance variable @obj outside the
> protection of the lock, it is possible that a thread could @obj being
> non-null, but not seeing the effects of initializing the object until
> much later!

Wow, that was poor grammar on my part. Let me try again:

For instance, if reading the instance variable @obj outside the protection of the lock, it is possible for a thread to see that @obj is non-null long before the effects of the object's initialize method have become visible to it!

-mental


Joel VanderWerf

6/11/2007 10:32:00 PM

0

MenTaLguY wrote:
> On Tue, 12 Jun 2007 03:13:39 +0900, "Nasir Khan" <rubylearner@gmail.com> wrote:
>> @@__ms_c_lock.synchronize { @__ms_lock = Mutex.new unless
>> @__ms_lock
>> } unless @__ms_lock
>>
>> What is the race condition...?
>
> It's known as the "double-checked locking" antipattern. Generally, anything like:
>
> unless @obj
> @lock.synchronize do
> unless @obj
> @obj = Whatever.new
> end
> end
> end
> @obj.some_method
>
> is wrong. This is because (when dealing with real multi-threading --
> not a major issue in Ruby yet, but it soon will be!) we depend on
> locks not only for preserving the order of operations, but ensuring
> that each thread has a consistent view of memory. For instance, when
> reading the instance variable @obj outside the protection of the
> lock, it is possible that a thread could @obj being non-null, but not
> seeing the effects of initializing the object until much later!

(...and later restated)
> For instance, if reading the instance variable @obj outside the
> protection of the lock, it is possible for a thread to see that @obj
> is non-null long before the effects of the object's initialize method
> have become visible to it!


So you're saying that, as the line

@obj = Whatever.new

is executed, it is possible (in some ruby implementations, but not MRI
1.8) that @obj will be assigned the Whatever instance *before* the #new
call completes? (That does seem to be what the wikipedia entry is
warning about.[1])

Scary! Your point that we should rely on the locking primitives for all
access (and hope that they get the memory barrier right) is well taken.
Which means always paying the sync cost:

def obj
@lock.synchronize do
@obj ||= Whatever.new
end
end

I wonder if the following will be a more efficient alternative, or worse
because of the singleton method:

def obj
@lock.synchronize do
@obj = Whatever.new
def self.obj
@obj
end
end
@obj
end

I suppose that has the same problem, depending on implementation, in
that the method definition could be reordered before the assignment...

[1] http://en.wikipedia.org/wiki/Double-check...

--
vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

MenTaLguY

6/12/2007 12:06:00 AM

0

On Tue, 12 Jun 2007 07:32:11 +0900, Joel VanderWerf <vjoel@path.berkeley.edu> wrote:
> So you're saying that, as the line
>
> @obj = Whatever.new
>
> is executed, it is possible (in some ruby implementations, but not MRI
> 1.8) that @obj will be assigned the Whatever instance *before* the #new
> call completes? (That does seem to be what the wikipedia entry is
> warning about.[1])

More or less. Some of this is a result of instruction reordering performed by both the compiler and the CPU, and some of this is the result of stuff not getting written out to shared memory right away. Either way, it's possible for one thread's operations to appear in a different order to other threads.

The effect of a memory barrier is to bring all threads involved into sync, so to speak, so that whatever is going on behind the scenes, they all see things happening in the same order. Memory barriers are the blue pill -- you really don't want to see how deep the concurrency rabbit hole goes.

> I wonder if the following will be a more efficient alternative, or worse
> because of the singleton method:
>
> def obj
> @lock.synchronize do
> @obj = Whatever.new
> def self.obj
> @obj
> end
> end
> @obj
> end
>
> I suppose that has the same problem, depending on implementation, in
> that the method definition could be reordered before the assignment...

There's also a race condition. Consider:

thread #1: enters #obj
thread #2: enters #obj
thread #2: acquires @lock
thread #1: waits for @lock
thread #2: @obj = Whatever.new (#<Whatever:0x1234abcd>)
thread #2: redefines obj
thread #2: releases @lock
thread #2: returns @obj (#<Whatever:0x1234abcd>)
thread #1: wakes up
thread #1: acquires @lock
thread #1: @obj = Whatever.new (#<Whatever:0xcafebeef>)
thread #1: redefines obj
thread #1: releases @lock
thread #1: returns @obj (#<Whatever:0xcafebeef>)

Sometimes when writing multi-threaded code, it helps to pretend that the thread scheduler hates your guts and has it in for you personally. I think you'd have to do something like this at least:

def obj
@lock.synchronize do
@obj ||= Whatever.new
def self.obj
@obj
end
@obj
end
end

But there's still the un-synchronized read of @obj from within the singleton method, which can even occur before @lock has been released by the writing thread. It might often work in practice, since the Ruby implementation is (hopefully) doing some kind of synchronization when reading/updating its method tables, preventing the CPU from reordering things across the method definition. But a lot depends on the memory model, and there's some pretty crazy stuff out there (*cough* Alpha *cough)...

It's probably best to stick to the simplest correct way:

def obj
@lock.synchronize do
@obj ||= Whatever.new
end
end

If the locking turns out to be a bottleneck, then the method's callers can cache its result in variables private to their respective threads, which will not require any locking at all to read.

-mental