ReaderWriterLockSlim的双重检查三重锁定模式

目录

介绍

ReaderWriterLockSlim的特征

问题

解决问题

一个新问题

下载示例


介绍

C#中,我们有lock关键字,它可以以独占方式锁定任何对象。也就是说,一旦一个线程持有锁,在第一个线程释放锁之前,其他线程将无法获取锁。

这种锁称为独占锁(有时称为完整锁),因为两个线程无法同时持有锁。

完全锁的替代方法是读写器锁。使用读写器锁,许多读卡器可以同时获得该锁,但写器锁实际上是一个独占锁。如果一个线程获取写锁,则其他线程无法获取读锁或写锁。

.NET首先有一个名为ReaderWriterLock的类,但它有点过时了,当我们想要读写器锁时,推荐一个名为ReaderWriterLockSlim的新类 。然而,这样的类被错误地使用了很多次,实际上变得比总是使用全锁的效率低。本文是关于理解此类问题并解决它的全部内容。

ReaderWriterLockSlim的特征

尽管该类的名称中包含“Slim”,但实际上ReaderWriterLockSlim并没有那么纤细,建议您在完成该类后使用其Dispose()方法释放其资源。

无论如何,一个有趣的特征是该类提供了三种锁定方法,而不仅仅是两种。

读锁和写锁的工作方式与简单的读写器锁相同,但还有第三种锁,名为upgradeable-read-lock

对于资源的创建/加载可能需要很长时间的情况,建议使用upgradeable-read-lock,因此我们不希望两个或多个线程并行执行相同的工作,但我们也不想阻止读取器读取对象,因为它们可能正在访问已加载的不同数据(例如在缓存方案中)。

因此,这种做法可以描述为:

  • 获取upgradeable-read-lock
  • 检查数据是否已经存在,如果存在,则返回它。如果不是...
  • 生成/加载数据
  • 获取写锁
  • 存储数据
  • 返回数据

问题

正如我所看到的,我刚才描述的模式有两个主要问题:

  1. 我没有谈论释放锁。当我们使用lock关键字时,当代码块结束时,锁会自动释放。使用ReaderWriterLockSlim,我们需要显式调用Exit所获取的适当类型的锁。
  2. 如果我们没有只是读取器的方法,那么这种模式就是不完整的。如果所有方法在持有可升级读锁的同时检查数据是否存在,则代码只是在执行较慢的独占锁,因为可升级读锁只允许并行获取普通读锁,而不允许其他可升级读锁。

解决问题

对于第一个问题,我们需要手动调用正确的Exit方法,我们可以使用扩展方法并返回实现IDisposable来执行退出的struct

例如,而不是执行以下操作:

rwLock.EnterUpgradeableReadLock();
try
{
  DoWhatsNeededHere();
}
finally
{
  rwLock.ExitUpgradeableReadLock();
}

我们可以做:

using (rwLock.UpgradeableLock())
  DoWhatsNeededHere();

为了实现这一点,我们需要这样的代码:

public readonly struct UpgradeableLockDisposer:
  IDisposable
{
  private readonly ReaderWriterLockSlim _rwLock;

  public UpgradeableLockDisposer(ReaderWriterLockSlim rwLock)
  {
    _rwLock = rwLock;
  }
  public void Dispose()
  {
    _rwLock.ExitUpgradeableReadLock();
  }
}

public static UpgradeableLockDisposer UpgradeableLock(this ReaderWriterLockSlim rwLock)
{
  rwLock.EnterUpgradeableReadLock();
  return new UpgradeableLockDisposer(rwLock);
}

structpublic的,并且UpgradeableLock正在按其类型返回它,而不是仅将其作为IDisposable返回,以避免任何装箱或任何内存分配。假设编译器正在执行正确的优化,则此帮助程序方法的性能应与使用try/finally的方法相同。

对于第二个问题,我们需要一个仔细检查的三重锁定模式。也就是说,我们需要做这样的事情:

using (rwLock.ReadLock())
{
  var data = TryGetData();
  if (data != null)
    return data;
}

using (rwLock.UpgradeableLock())
{
  // We try to get the data again, as it might have been added while we
  // tried to get the upgradeable lock.

  var data = TryGetData();
  if (data != null)
    return data;
  
  data = CallMethodToGenerateTheData();

  using (rwLock.WriteLock())
    StoreData(data);

  return data.
}

请注意,我们使用3种锁类型。

首先,我们只使用读锁。这意味着许多线程可以同时访问数据。假设数据不存在,我们需要释放读锁,然后获取另一个锁。

其次,我们获取可升级读锁,并需要再次检查数据,因为它可能是在我们获取第二个锁时添加的。假设数据不存在,我们可以加载/生成数据,因为没有其他线程会这样做,因为没有两个线程可以同时具有可升级锁。

最后,当我们获得数据时,我们需要获得写锁,这意味着所有具有读锁(如果有)的线程都需要释放它们的锁,然后写入数据,然后再返回。

多亏了using子句,我们不需要担心任何Exit调用,但在离开using作用域时会适当地完成。

一个新问题

三重锁定方法可能听起来有些过分。这会扼杀性能,对吧?

可能吧。正如我之前所说,ReaderWriterLockSlim不是那么纤细,获取完全锁并读取值可能比仅获取读锁更快。这是促使我编写替代锁定类的原因之一,我在托管线程同步一文中介绍了这些类。

但仍然谈论ReaderWriterLockSlim,试图避免读锁是禁忌。如果只使用可升级锁,这类似于一直做全锁......但速度较慢。

如果加载数据实际上很快,则只执行读锁和写锁,而不使用可升级锁可能是一种有效的方法。但是,如果是这样的话,也许使用ConcurrentDictionary(假设我们有键控数据)或Lazy(假设单个数据)会更好。

在任何情况下,如果您使用的是ReaderWriterLockSlim,则必须注意必须使用读锁。仅使用upgradeable-lockwrite-lock是错误的。

此外,即使三重锁模式看起来太多,在加载数据后,代码也只会使用读锁(可能并行)来读取数据,并且不会再次使用可升级锁和写锁。

下载示例

下载示例有一个ReaderWriterLockSlimExtensions类,其中三种锁类型作为IDisposable返回,因此您可以使用该using子句自动释放锁。

如果你愿意,你可以将其文件添加到任何项目中。如果您愿意,可以随意将该类放入命名空间中。

https://www.codeproject.com/Articles/5360141/The-Double-Checked-Triple-Lock-Pattern-for

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值