5

最近我一直在重构我的一些 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    
    }
}

我的观点是,没有其他线程应该再次尝试初始化该项目,而一旦值被初始化,许多线程应该同时读取。如果获取了写锁,可升级读锁应该阻塞所有读取器,因此在初始化对象时,行为将类似于在可升级读锁开始的地方有一个锁语句。初始化后,可升级读锁将允许多个线程,因此不会出现等待每个线程的性能损失。

我还在这里阅读了一篇文章,说 volatile 会导致在读取之前和写入之后自动插入内存屏障,因此我假设在读取和写入之间只有一个手动定义的屏障就足以确保正确读取 _valueReference 对象。我很乐意感谢您对使用这种方法的建议和批评。

4

2 回答 2

2

To emphasize the point @Mannimarco makes: if this is the only access point to the Value, and it looks that way, then your whole ReaderWriterLockSlim setup is no better than a simple Monitor.Enter / Monitor.Leave approach. It is a lot more complicated though.

So I believe the following code is equivalent in function and efficiency:

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; 
    }
  }    
}
于 2011-06-13T19:02:15.077 回答
2

警告:一次只有一个线程可以进入 UpgradeableReadLock 模式。查看ReaderWriterLockSlim。因此,如果在第一个线程进入写入模式并创建对象时线程堆积起来,那么在(希望)解决备份之前,您将遇到瓶颈。我会认真建议使用静态初始化程序,它会让你的生活更轻松。

编辑:根据需要重新创建对象的频率,我实际上建议使用 Monitor 类及其 Wait 和 Pulse 方法。如果需要重新创建该值,让线程等待一个对象并脉冲另一个对象,让工作线程知道它需要唤醒并创建一个新对象。创建对象后,PulseAll 将允许所有读取器线程唤醒并获取新值。(理论上)

于 2011-06-13T18:54:44.853 回答