最近我一直在重构我的一些C#代码,并且发现了一些双重检查锁定实践。那时我不知道这是一个不好的做法,我真的想摆脱它。
问题是我有一个应该被懒惰地初始化并且经常被许多线程访问的类。我也不想将初始化移动到静态初始化器,因为我计划使用弱引用来保持初始化对象在内存中保持太长时间。但是,如果需要,我想“恢复”对象,确保以线程安全的方式发生这种情况。
我想知道在第一次检查之前是否在C#中使用ReaderWriterLockSlim并输入UpgradeableReadLock,然后如果需要,为初始化输入写锁定将是可接受的解决方案。以下是我的想法:
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
}
}
我的观点是,没有其他线程应该尝试再次初始化项目,而一旦初始化值,许多线程应该同时读取。如果获取了写锁,则可升级读锁应阻止所有读取器,因此在初始化对象时,行为类似于具有可升级读锁开始的锁语句。初始化之后,可升级读锁定将允许多个线程,因此等待每个线程的性能不会出现。
我还读了一篇文章here,说volatile会导致内存障碍在读取之前和写入之后自动插入,所以我假设读取和写入之间只有一个手动定义的屏障就足以确保_valueReference对象被正确读取。我很乐意感谢您对使用这种方法的建议和批评。
答案 0 :(得分:2)
警告:一次只能有一个线程进入UpgradeableReadLock模式。查看ReaderWriterLockSlim。因此,如果线程在第一个线程进入写入模式并且创建对象时堆积起来,那么在备份(希望)解决之前,您将有一个瓶颈。我认真建议使用静态初始化程序,它会让你的生活更轻松。
编辑:根据需要重新创建对象的频率,我实际上建议使用Monitor类及其Wait和Pulse方法。如果需要重新创建值,请让对象上的线程等待,然后使用Pulse另一个对象让工作线程知道它需要唤醒并创建一个新对象。一旦创建了对象,PulseAll将允许所有读取器线程唤醒并获取新值。 (理论上)
答案 1 :(得分:2)
强调@Mannimarco的观点:如果这是Value的唯一访问点,并且它看起来那样,那么整个ReaderWriterLockSlim设置并不比简单的Monitor.Enter / Monitor.Leave方法好。但它要复杂得多。
所以我相信以下代码在功能和效率上是等价的:
private WeakReference _valueReference = new WeakReference(null);
private object _locker = new object();
public MyType Value
{
get
{
lock(_locker) // also provides the barriers
{
value = _valueReference.Target;
if (!_valueReference.IsAlive)
{
_valueReference = new WeakReference(value = InitializeMyType());
}
return value;
}
}
}