c#线程同步系列(二) c#中ReaderWriterLock的使用

时间:2022-05-14 19:30:54

到这一篇,在Windows下主流的线程同步方法已经都讲过了,包括穿插提到的Interlocked类,那都是我们传统的曾经学到过的概念。除此之外,.Net提供了一些特有的东西来帮助我们方便地完成代码,于是便有这一篇中要讨论的读写锁。

ReaderWriterLock锁的好处

  它跟Monitor一样,是.Net的原生类,不再与操作系统有什么瓜葛。回想Monitor、EventWaitHandle两篇中,关于生产者、消费者和糖罐的例子,无论是一个消费者一个生产者、还是一个消费者和多个生产者,由于使用Monitor/lock的原因,一个时刻总是只有一个线程在对糖罐进行互斥的访问。这样其实会对吞吐量造成影响,如果有一个对实时性要求比较高的场景,在各种处理线程增加到一等数目后,处理速度的瓶颈就可能变为对资源的互斥访问上。

  在某些场景里,多个并发的读访问并不会有什么问题,这就是ReaderWriterLock针对Monitor改进之处。以下摘自MSDN:

ReaderWriterLock 用于同步对资源的访问。在任一特定时刻,它允许多个线程同时进行读访问,或者允许单个线程进行写访问。在资源不经常发生更改的情况下,ReaderWriterLock 所提供的吞吐量比简单的一次只允许一个线程的锁(如 Monitor)更高。

在多数访问为读访问,而写访问频率较低、持续时间也比较短的情况下,ReaderWriterLock 的性能最好。多个读线程与单个写线程交替进行操作,所以读线程和写线程都不会长时间阻止。

注意
长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。

一个线程可以持有读线程锁或写线程锁,但是不能同时持有两者。若要获取写线程锁,请使用 UpgradeToWriterLock 和 DowngradeFromWriterLock,而不要通过释放读线程锁的方式获取。

递归锁请求会增加锁上的锁计数。

读线程和写线程将分别排入各自的队列。当线程释放写线程锁时,此刻读线程队列中的所有等待线程都将被授予读线程锁;当已释放所有读线程锁时,写线程队列中处于等待状态的下一个线程(如果存在)将被授予写线程锁,依此类推。换句话说,ReaderWriterLock 在一组读线程和一个写线程之间交替进行操作。

当写线程队列中有一个线程在等待活动读线程锁被释放时,请求新的读线程锁的线程会排入读线程队列。即使它们能和现有的阅读器锁持有者共享并发访问,也不会给它们的请求授予权限;这有助于防止编写器被阅读器无限期阻止。

大多数在 ReaderWriterLock 上获取锁的方法都采用超时值。使用超时可以避免应用程序中出现死锁。例如,某个线程可能获取了一个资源上的写线程锁,然后请求第二个资源上的读线程锁;同时,另一个线程获取了第二个资源上的写线程锁,并请求第一个资源上的读线程锁。如果不使用超时,这两个线程将出现死锁。

如果超时间隔过期并且没有授予锁请求,则此方法通过引发 ApplicationException 将控制返回给调用线程。线程可以捕捉此异常并确定下一步要进行的操作。

  这段描述还算比较清楚,不会给人带来太多困惑,我只是想提醒几点:

  • 请确信在你的使用场景中,读的并发访问是允许的。我们之前的生产者、消费者和糖罐的例子并不适合使用ReadWriterLock,因为生产者和消费者都在“写”糖罐,只是一个插入一个删除而已。
  • 这里的并发只是针对读操作,读写本身还是互斥的。在读锁被获取时是无法得到写锁的,反之亦然。所以它适合于“写访问频率较低、持续时间也比较短的情况”。如果写时间较长,也就意味着对这个资源总的(读和写)访问频率较低,那么本来也就没有吞吐量低的问题了。如果出现这种状况,你可以尝试把占用时间的读写操作再次安排到其它工作线程中去,尽量缩短对资源的占用时间。
  • 微软竟然对超时采用抛出异常的方式,并且居然说“可以捕捉此异常并确定下一步要进行的操作”……你见过用异常控制程序流程的设计吗?!(我对微软的抱怨是不是太多了?)

ReaderWriterLock的使用方法

  好了,让我们继续上一篇言简意赅的偷懒风格,浏览下ReaderWriterLock的主要方法:

  • AcquireReaderLock():获取读线程锁。
  • AcquireWriterLock():获取写线程锁。
  • ReleaseReaderLock():减少锁计数,计数到达零时释放锁。ReleaseReaderLock将减少锁计数。如果线程持有写线程锁,调用 ReleaseReaderLock 与调用 ReleaseWriterLock 具有相同的效果。如果线程没有锁,调用 ReleaseReaderLock 会引发 ApplicationException。
  • ReleaseWriterLock():将减少写线程锁计数。计数变为零时释放写线程锁。如果线程持有读线程锁或没有锁,调用 ReleaseWriterLock 会引发 ApplicationException。
  • ReleaseLock():释放锁,不管线程获取锁的次数如何。

  差不多了吧,其它Member可以参见这里

Sample Code

  是的,仍然偷懒了,因为仍然想不出好的有些实际意义的例子。MSDN在关于ReaderWriterLock类本身的介绍中,以及以上各方法的说明里都给出了若干Sample Code,请自行参考吧。

 

相关文章