The broken double-check lock: It's broken but it works.

The broken double-check lock

In any multi-threading scenario, it's pretty common to have critical sections where all threads have to be mutually exclusive. In C#, it's pretty straight forward; thanks to Monitors and their mirror-image, the lock syntax.

object SyncRoot = new object();

// ...

lock (SyncRoot)
{
    // Critical section
}

Which is shorthand for (pre-.NET 4):

object SyncRoot = new object();

// ...

Monitor.Enter(SyncRoot);

try
{
    // Critical section
}
finally
{
    Monitor.Exit(SyncRoot);
}

It looks like pretty basic stuff and it is. Consider the following double-check lock for lazy initialization:

object SyncRoot = new object();
object TheLazyOne = null;

// ...

if (TheLazyOne == null)
{
    lock (SyncRoot)
    {
        if (TheLazyOne == null)
            TheLazyOne = new object();
    }
}

For those who are not familiar with double-check locks, it's a very convenient way to avoid most of the performance impact due to locking (locking is often expensive). The "exceptional" condition is checked first; if it succeeds, we proceed to enter a critical section and then check for the exceptional condition again.

The previous code excerpt looks good at first glance. However, it is broken. The compiler might try to optimize it and strip one of the condition (let's not get into details about the memory model here...). We can certainly deduct the following:

  • If the compiler removes the outer condition, we're back to a basic lock without double-checking
  • If it removes the inner condition, the implementation is broken (it can happen more than once since the condition isn't part of the critical section)

This particular case is solvable using the too-often-forgotten volatile keyword. Simple adding this modifier to the object involved in the condition (TheLazyOne in our case) is enough to tell the compiler not to mess too much with our variable since multiple threads might try to modify its value.

volatile object TheLazyOne = null;

Singletons are also a common case of lazy initialization. See Jon Skeet's very thorough article about Singleton implementations (which also mentions the broken double-check lock implementation).

Disclaimer: In no way do I intend to take credit for "discovering" what seems to be "broken" above; it's well known and also well documented in the MSDN documentation (see this link for detailed information). Also, as of .NET 2.0, the above is not broken (using Microsoft's implementation); but it's still broken according to the ECMA relaxed memory model (the standard).

Posted by: Bryan Menard
Last revised: 22 Oct, 2011 06:37 PM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.