首页 / 软件开发 / .NET编程技术 / 并发数据结构: .NET Framework中提供的读写锁
并发数据结构: .NET Framework中提供的读写锁2011-08-07 博客园 Angel Lucifer在多线程编程时,开发人员经常会遭遇多个线程读写某个资源的情况。这就需要进行线程同步来保证 线程安全。一般情况下,我们的同步措施是使用锁机制。但是,假如线程只对资源进行读取操作,那么根 本不需要使用锁;反之,假如线程只对资源进行写入操作,则应当使用互斥锁(比如使用 Monitor 类等) 。还有一种情况,就是存在多个线程对资源进行读取操作,同时每次只有一个线程对资源进行独占写入操 作。这正是本文主题--读写锁的用武之地。ReaderWriterLock 类.NET Framework BCL 在 1.1 版本时,给我们提供了一个 ReaderWriterLock 类来面对此种情景。但 是很遗憾,Microsoft 官方不推荐使用该类。Jeffrey Richter 也在他的《CLR via C#》一书中对它进行 了严厉的批判。下面是该类不受欢迎的主要原因:性能。这个类实在是太慢了。比如它的 AcquireReaderLock 方法比 Monitor 类的 Enter 方法要慢 5 倍左右,而等待争夺写锁甚至比 Monitor 类慢 6 倍。策略。假如某个线程完成写入操作后,同时面临读线程和写线程等待处理。ReaderWriterLock 会优先 释放读线程,而让写线程继续等待。但我们使用读写锁是因为存在大量的读线程和非常少的写线程,这样 写线程很可能必须长时间地等待,造成写线程饥饿,不能及时更新数据。更槽糕的情况是,假如写线程一 直等待,就会造成活锁。反之,我们让 ReaderWriterLock 采取写线程优先的策略。如果存在多个写线程 ,而读线程数量稀少,也会造成读线程饥饿。幸运的是,现实实践中,这种情况很少出现。一旦发生这种 情况,我们可以采取互斥锁的办法。递归。ReaderWriterLock 类支持锁递归。这就意味着该锁清楚的知道目前哪个线程拥有它。假如拥有 该锁的线程递归尝试获得该读写锁,递归算法允许该线程获得该读写锁,并且增加获得该锁的计数。然而 该线程必须释放该锁相同的次数以便线程不再拥有该锁。尽管这看起来是个很好的特性,但是实现这个“ 特性”代价太高。首先,因为多个读线程可以同时拥有该读写锁,这必须让该锁为每个线程保持计数。此 外,还需要额外的内存空间和时间来更新计数。这个特性对 ReaderWriterLock 类可怜的性能贡献极大。 其次,有些良好的设计需要一个线程在此处获得该锁,然后在别处释放该锁(比如 .NET 的异步编程架构) 。因为这个递归特性,ReaderWriterLock 不支持这种编程架构。资源泄漏。在 .NET 2.0 之前的版本中, ReaderWriterLock 类会造成内核对象泄露。这些对象只有 在进程终止后才能再次回收。幸运的是,.NET 2.0 修正了这个 Bug 。此外,ReaderWriterLock 还有个令人担心的危险的非原子性操作。它就是 UpgradeToWriteLock 方法 。这个方法实际上在更新到写锁前先释放了读锁。这就让其他线程有机会在此期间乘虚而入,从而获得读 写锁且改变状态。如果先更新到写锁,然后释放读锁。假如两个线程同时更新将会导致另外一个线程死锁 。所以 Microsoft 决定构建一个新类来一次性解决上述所有问题,这就是 ReaderWriterLockSlim 类。 本来可以在原有的 ReaderWriterLock 类上修正错误,但是考虑到兼容性和已存在的 API ,Microsoft 放弃了这种做法。当然也可以标记 ReaderWriterLock 类为 Obsolete,但是由于某些原因,这个类还有 存在的必要。ReaderWriterLockSlim 类新的 ReaderWriterLockSlim 类支持三种锁定模式:Read,Write,UpgradeableRead。这三种模式对 应的方法分别是 EnterReadLock,EnterWriteLock,EnterUpgradeableReadLock 。再就是与此对应的 TryEnterReadLock,TryEnterWriteLock,TryEnterUpgradeableReadLock,ExitReadLock, ExitWriteLock,ExitUpgradeableReadLock。Read 和 Writer 锁定模式比较简单易懂:Read 模式是典型 的共享锁定模式,任意数量的线程都可以在该模式下同时获得锁;Writer 模式则是互斥模式,在该模式 下只允许一个线程进入该锁。UpgradeableRead 锁定模式可能对于大多数人来说比较新鲜,但是在数据库 领域却众所周知。这个新的读写锁类性能跟 Monitor 类大致相当,大概在 Monitor 类的 2 倍之内。而且新锁优先让写 线程获得锁,因为写操作的频率远小于读操作。通常这会导致更好的可伸缩性。起初, ReaderWriterLockSlim 类在设计时考虑到相当多的情况。比如在早期 CTP 的代码还提供了 PrefersReaders, PrefersWritersAndUpgrades 和 Fifo 等竞争策略。但是这些策略虽然添加起来非常 简单,但是会导致情况非常的复杂。所以 Microsoft 最后决定提供一个能够在大多数情况下良好工作的 简单模型。