views:

165

answers:

5

I used this pattern in a few projects, (this snipped of code is from CodeCampServer), I understand what it does, but I'm really interesting in an explanation about this pattern. Specifically:

  1. Why is the double check of _dependenciesRegistered.
  2. Why to use lock (Lock){}.

Thanks.

public class DependencyRegistrarModule : IHttpModule
{
    private static bool _dependenciesRegistered;
    private static readonly object Lock = new object();

    public void Init(HttpApplication context)
    {
        context.BeginRequest += context_BeginRequest;
    }

    public void Dispose() { }

    private static void context_BeginRequest(object sender, EventArgs e)
    {
        EnsureDependenciesRegistered();
    }

    private static void EnsureDependenciesRegistered()
    {
        if (!_dependenciesRegistered)
        {
            lock (Lock)
            {
                if (!_dependenciesRegistered)
                {
                    new DependencyRegistrar().ConfigureOnStartup();
                    _dependenciesRegistered = true;
                }
            }
        }
    }
}
+12  A: 

This is the Double-checked locking pattern.

The lock statement ensures that the code inside the block will not run on two threads simultaneously.
Since a lock statement is somewhat expensive, the code checks whether it's already been initialized before entering the lock.
However, because a different thread might have initialized it just after the outer check, it needs to check again inside the lock.

Note that this is not the best way to do it.

SLaks
+1 especially for the "this is not the best way to do it" note
Dinah
In the wikipedia article: The pattern, when implemented in some language/hardware combinations, can be unsafe.
Nelson
This is not the best way to implement a singleton, because you will most likely get it wrong. As I believe this implementation is flawed since it's missing the volatile on the variable. This pattern when implemented properly might be a better choice in some situations.
JoshBerke
The *"this isn't the best way"* link doesn't actually seem to apply, since its argument against this pattern is *"the singleton constructor isn't guaranteed to be completed before the variable is set."* However, this is no variable being set to a constructor here, so the double-checked lock works fine, even without `volatile`
BlueRaja - Danny Pflughoeft
@BlueRaja: This way will still be slower than a static constructor.
SLaks
@SLacks: I don't believe the quoted blog post. There must be a lock somewhere or the code generated by the CLR for this is busted. The lock is implied on the static constructor for the class. The CLR prevents two threads that access the class at the same time from calling the class constructor more than once implying that the other thread waits.
chuckj
@chuckj: I realize that. However, that lock needs to be taken anyway. By using the static constructor, you avoid an extra lock.
SLaks
@Slacks: That is not true. If you don't have a static constructor the lock is not taken. Introducing a statically initialized variable introduces an implicit static constructor and introduces a lock. This is certainly a fine way to do this but I don't believe it is any faster than the double check.
chuckj
@chuckj: He already has a static initializer – the `Lock` object.
SLaks
@SLacks: The static constructor can be avoided by using typeof(DependencyRegistrarModule) instead of Lock (and then removing lock). This has the additional benefit of removing the JIT generated calls to check if class has been constructed whenever the class is referenced (if the method is JIT'ed before the class is initialized, after the checks are omitted). Moving the "then" part of the initialization to a separate method also elimitates the need for a stack-frame in the get method when the variable is already initialized. I got the get down to 4 instructions on the initialized path (386).
chuckj
@Slacks: BTW, if ConfigureOnStartup() is a void method (which from the context it probably is) the static intializer pattern will not help you. You need to use a double test to for it to be lazy.
chuckj
I retract the above (where is delete when you need it). When JIT'ed the best pattern to call ConfigureOnStartup() in a static constructor of a nested class and have an empty static method called Ensure() that will ensure the class class constructor is invoked but gets erased entirely if the class is already constructed. When NGEN'ed however, the double test is better because NGEN must generate the call to the class constructor test where the double test can be optimized better to inline the test (as described above).
chuckj
+2  A: 

The double-check is because two threads could hit EnsureDependenciesRegistered at the same time, both find it isn't registered, and thus both attempt to get the lock.

lock(Lock) is essentially a form of mutex; only one thread can have the lock - the other must wait until the lock is released (at the end of the lock(...) {...} statement).

So in this scenario, a thread might (although unlikely) have been the second thread into the lock - so each must double-check in case it was the second, and the work has already been done.

Marc Gravell
+1  A: 

It's a matter of performance.

The initial test lets it bail out quickly if the job is already done. At this point it does the potentially expensive lock but it has to check it again as another thread could have already registered it.

Loren Pechtel
+1  A: 

The double checked locking pattern is roughly:

you have an operation that you want to conditionally perform once

if (needsToDoSomething) {
   DoSomething();
   needsToDoSomething = false;
}

however, if you're running on two threads, both threads might check the flag, and perform the action, before they both set the flag to false. Therefore, you add a lock.

lock (Lock) {
    if (needsToDoSomething) {
       DoSomething();
       needsToDoSomething = false;
    }
}

however, taking a lock every time you run this code might be slow, so you decide, lets only try to take the lock when we actually need to.

 if (needsToDoSomething)
    lock (Lock) {
        if (needsToDoSomething) {
           DoSomething();
           needsToDoSomething = false;
        }
    }

You can't remove the inner check, because once again, you have the problem that any check performed outside of a lock can possibly turn out to be true twice on two different threads.

Jimmy
+1  A: 

The lock prevents two threads from running ConfigureOnStartup(). Between the if (!_dependenciesRegistered) and the point that ConfigureOnStartup() sets _dependenciesRegistered = true, another thread could check if it's registered. In other words:

  1. Thread 1: _dependenciesRegistered == false
  2. Thread 2: _dependenciesRegistered == false
  3. Thread 1: ConfigureOnStartup() / _dependenciesRegistered = true;
  4. Thread 2: Doesn't "see" that it's already registered, so runs ConfigureOnStartup() again.
Nelson