再次仔细检查锁定和 C#
最近,我一直在重构一些 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
}
}
我的观点是,任何其他线程都不应该尝试再次初始化该项目,而一旦初始化该值,许多线程应该同时读取。如果获取了写锁,可升级读锁应该阻塞所有读取器,因此在初始化对象时,行为将类似于在可升级读锁开始处使用一个锁定语句。初始化后,可升级读锁将允许多个线程,因此不会出现等待每个线程的性能影响。
我还读过一篇文章这里说易失性会导致在读取之前自动插入内存屏障写入后,因此我假设读取和写入之间只有一个手动定义的屏障就足以确保正确读取 _valueReference 对象。我很乐意感谢您对使用这种方法的建议和批评。
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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
为了强调 @Mannimarco 提出的观点:如果这是该值的唯一访问点,并且看起来是这样,那么您的整个 ReaderWriterLockSlim 设置并不比简单的 Monitor.Enter / Monitor.Leave 方法更好。但情况要复杂得多。
所以我相信下面的代码在功能和效率上是等效的:
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:
警告:一次只能有一个线程进入 UpgradeableReadLock 模式。请查看 ReaderWriterLockSlim。因此,如果在第一个线程进入写入模式并创建对象时线程堆积,那么在(希望)解决备份之前,您将遇到瓶颈。我强烈建议使用静态初始化器,它会让你的生活更轻松。
编辑:根据需要重新创建对象的频率,我实际上建议使用 Monitor 类及其 Wait 和 Pulse 方法。如果需要重新创建该值,请让线程等待一个对象并脉冲另一个对象,让工作线程知道它需要唤醒并创建一个新对象。创建对象后,PulseAll 将允许所有读取器线程唤醒并获取新值。 (理论上)
A warning: only a single thread can enter UpgradeableReadLock mode at a time. Check out ReaderWriterLockSlim. So if threads pile up while the first thread enters write mode and creates the object, you'll have a bottle neck until the backup is (hopefully) resolved. I would seriously suggest using a static initializer, it will make your life easier.
EDIT: Depending on how often the object needs to be recreated, I would actually suggest using the Monitor class and its Wait and Pulse methods. If the value needs to be recreated, have the threads Wait on an object and Pulse another object to let a worker thread know that it needs to wake up and create a new object. Once the object has been created, PulseAll will allow all the reader threads to wake up and grab the new value. (in theory)