Friday, February 22, 2008

Efficient singleton initialization with Interlocked

We had (are still having) an interesting discussion over at Sven-Torben Janus's blog. The discussion is about singletons, but it reminded me of a pattern I've used which has more general application. This is how it might be used to initialize a singleton:


using System.Threading;

public class Singleton
{
private Singleton() { /* ... */ }

const int NotInitialized = 0;
const int Initializing = 1;
const int Initialized = 2;
static int state;
static Singleton instance;

public static Singleton Instance
{
get
{
if (instance != null)
return instance;
while (true)
{
switch (Interlocked.CompareExchange(ref state, Initializing, NotInitialized))
{
case NotInitialized:
Interlocked.Exchange(ref instance, new Singleton());
Interlocked.Exchange(ref state, Initialized);
return instance;
case Initializing:
Thread.Sleep(0); // yield and loop until initialization is finished
break;
case Initialized:
return instance; // does this need to be interlocked?
}
}
}
}
}


This is a much cheaper pattern than the Java-style double-checked locking pattern (which doesn't work on the CLR). However, I wouldn't use it to initialize singletons, because the CLR already guarantees lazy first-time initialization for static members! Which means that all you need do is this:


public class Singleton
{
static Singleton instance = new Singleton();
public static Singleton Instance { get { return instance; } }
}


However, the pattern is still useful. I use it for service initialization, for example where I have to run a costly database query before I can start responding to service requests:


public void GuardInitialized()
{
switch (Interlocked.CompareExchange(ref state, Initializing, NotInitialized))
{
case NotInitialized:
ThreadPool.QueueUserWorkItem(this.CostlyInitializationFunction);
throw new FaultException(new ServerNotReady());
case Initializing:
throw new FaultException(new ServerNotReady());
case Initialized:
break;
}
}


As always, your mileage may vary.

Update: Some actual timing tests revealed this this wasn't so efficient after all. In fact, until I added the "if (instance != null) return instance;" check to the singleton example, it was actually slower than lock() or memory barrier techniques! With the check, it's faster, but only by 5-10%. The JITter doesn't recognized the interlocked operations as intrinsic and inline them. I suspect that the unexpectedly slow performance is due to pinning and CLR synchronization overhead. Would anyone in the know like to comment?

Further update: I exchanged a few mails with Rico Mariani, and learned that the Monitor primitive actually implements a lock-promotion protocol. Which means that unless there's actually contention for the lock, it's effectively the same thing as an InterlockedCompareExchange operation. So for all practical purposes, lock() and Interlocked.CompareExchange are equivalent. The only time you'll see a real performance difference is when you're writing code where contention is actually a significant factor. So there you go :-).

Labels: , , ,

0 Comments:

Post a Comment

<< Home