Recently I have been refactoring some of my C# code and I found a few double-checked locking practices taking place. I didn't know it was a bad practice back then and I really want to get rid of it.
The problem is that I have a class that should be lazily initialized and frequently accessed by lots of threads. I also do not want to move the initialization to a static initializer, because I am planning to use a weak reference to keep the initialized object from staying too long in the memory. However, if needed, I want to 'revive' the object ensuring this happens in a thread-safe manner.
I was wondering if using a ReaderWriterLockSlim in C# and enter an UpgradeableReadLock before the first check, and then if necessary enter a write lock for the initialization would be an acceptable solution. Here is what I'm having in mind:
public class LazyInitialized
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private volatile WeakReference _valueReference = new WeakReference(null);
public MyType Value
{
get
{
MyType value = _valueReference.Target as MyType;
_lock.EnterUpgradeableReadLock();
try
{
if (!_valueReference.IsAlive) // needs initializing
{
_lock.EnterWriteLock();
try
{
if (!_valueReference.IsAlive) // check again
{
// prevent reading the old weak reference
Thread.MemoryBarrier();
_valueReference = new WeakReference(value = InitializeMyType());
}
}
finally
{
_lock.ExitWriteLock();
}
}
}
finally
{
_lock.ExitUpgradeableReadLock();
}
return value;
}
}
private MyType InitializeMyType()
{
// code not shown
}
}
My point is that no other thread should try to initialize the item once again, while many threads should read simultaneously once the value is initialized. The upgradeable read lock should block all readers if the write lock is acquired, therefore while the object is being initialized, the behavior will be similar to having a lock statement where the upgradeable read lock begins. After the initialization the Upgradeable read lock will permit multiple threads therefore the performance hit of waiting each thread will not be present.
I also read an article here saying that volatile causes memory barriers to be automatically inserted before read and after write, so I assume only one manually defined barrier between the read and the write will be enough to ensure that the _valueReference object is correctly read. I will gladly appreciate your advices and criticism for using this approach.