65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 原生实现类似 .NET 的 ReaderWriterLock 类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (26投票s)

2006年2月21日

CPOL

9分钟阅读

viewsIcon

137300

downloadIcon

2144

一个在 C++ 中非常高效的读写锁类,它类似于 .NET 的 ReaderWriterLock

引言

读写锁(ReaderWriterLock)机制用于同步对资源的访问。在任何给定时间,它允许多个线程并发读取,或者只允许一个线程写入。在资源不经常更改的情况下,读写锁比简单的逐一锁定(例如 CriticalSectionMutex)能提供更好的吞吐量。

背景

.NET Framework 中的 System.Threading.ReaderWriterLock 提供了一种 Win32 没有的新同步方法。如果您从未听说过读写锁的概念,那么 .NET 文档是一个很好的起点。以下是我对 .NET Framework 中实现的 ReaderWriterLock 特性的总结:

  1. ReaderWriterLock 用于同步对资源的访问。在任何给定时间,它允许多个线程并发读取,或者只允许一个线程写入。
  2. 该类在大多数访问都是读取、而写入操作**不频繁**且**持续时间短**的情况下效果最佳。在这种情况下,ReaderWriterLock 比简单的逐一锁定(如 CriticalSectionMutex)能提供更好的吞吐量。
  3. 当一个写入者正在等待活动读取锁被释放时,请求新读取锁的线程**必须在读取队列中等待**。即使它们可以与现有的读取锁持有者共享并发访问,它们的请求也不会被授予;这有助于保护写入者免受读取者造成的无限期阻塞。
  4. 支持**重入**(允许同一线程多次调用 AcquireReaderLock/AcquireWriterLock)和**锁升级**(在拥有读取锁时调用 UpgradeToWriterLock)。

C++ 读写锁类

我对 读写锁 机制的实现具有 .NET 类的所有特性,但“锁句柄”除外。此外,此实现中有两个类:每个类都有其自身的便利性(当然也有不便之处)。以下是它们重要方法的简要描述:

CReaderWriterLockNonReentrance

AcquireReaderLock 获取读取锁。调用此方法时,请确保调用线程尚未拥有任何锁(读锁或写锁);否则可能会发生死锁。
ReleaseReaderLock 释放读取锁。调用此方法时,请确保调用线程已拥有读取锁;否则对象的行为将是未定义的。
AcquireWriterLock 获取写入锁。调用此方法时,请确保调用线程尚未拥有任何锁(读锁或写锁);否则可能会发生死锁。
ReleaseWriterLock 释放写入锁。调用此方法时,请确保调用线程已拥有写入锁;否则对象的行为将是未定义的。
DowngradeFromWriterLock 以原子操作释放写入锁并获取读取锁。调用此方法时,请确保调用线程已拥有写入锁;否则对象的行为将是未定义的。
UpgradeToWriterLock

释放读取锁并获取写入锁。如果发生“超时”,此方法将在返回前自动重新获取读取锁。换句话说:如果“升级”因“超时”而失败,调用线程仍将持有(拥有)读取锁。调用此方法时,请确保线程已拥有读取锁;否则对象的行为将是未定义的。
重要提示:如果“timeout” != 0,则在方法返回之前,其他线程可能写入资源,无论调用线程是否成功升级。

本文附带的演示项目还显示了 CReaderWriterLockNonReentrance 类的一个非预期行为(在一个线程中调用 AcquireXXX ,然后在另一个线程中调用 ReleaseXXX )。虽然不推荐使用此非预期功能,但在某些情况下它非常有用;请谨慎使用。

CReaderWriterLock

AcquireReaderLock 一个线程可以多次调用 AcquireReaderLock,每次都会增加读取锁计数器。您必须为每次调用 AcquireReaderLock 调用一次 ReleaseReaderLock。或者,您可以调用 ReleaseAllLocks 将锁计数立即减少到零。
注意:当满足以下两个条件之一时,读取锁请求将始终立即获得:
a) 当前线程已拥有写入锁;这是为了防止线程阻塞自身。
b) 当前线程已拥有读取锁(支持递归锁)
ReleaseReaderLock 减少读取锁计数器。当计数器达到零时,锁被释放。调用此方法时,请确保调用线程已拥有读取锁;否则将在 DEBUG 模式下引发异常。
AcquireWriterLock

一个线程可以多次调用 AcquireWriterLock,每次都会增加写入锁计数器。您必须为每次调用 AcquireWriterLock 调用一次 ReleaseWriterLock。或者,您可以调用 ReleaseAllLocks 将锁计数立即减少到零。
注意:
a) 为防止线程阻塞自身,当当前线程已拥有写入锁时,写入锁请求将始终立即获得。
b) 如果调用线程已拥有读取锁但未拥有写入锁,它将隐式“升级”为拥有写入锁。
重要提示:如果进行了隐式“升级”且“timeout” != 0,则在方法返回之前,其他线程可能写入资源,无论调用线程是否成功升级。

ReleaseWriterLock 减少写入锁计数器。当计数器达到零时,锁被释放。调用此方法时,请确保调用线程已拥有写入锁;否则将在 DEBUG 模式下引发异常。
注意:如果对象检测到此请求对应于之前的自动升级,它也将自动降级(释放写入锁但仍保留读取锁)。
ReleaseAllLocks 将调用线程的所有锁计数器(写入计数器和读取计数器)重置为零,并释放写入锁和读取锁,无论该线程拥有多少次读取锁或写入锁。
注意:之后,任何对 ReleaseWriterLockReleaseReaderLock 的调用将在 DEBUG 模式下引发异常。
GetCurrentThreadStatus 检索调用线程的锁计数器(读取计数器和写入计数器)。

正如有些人可能注意到的,没有像 **CReaderWriterLockNonReentrance** 类那样的“升级”或“降级”方法。实际上,我们不需要这些方法,因为这些操作(“升级”或“降级”)将是隐式和自动进行的。

一些辅助类

CAutoReadLock(T)CAutoWriteLock(T) 允许线程在一个代码块中获取锁,而不必担心在遇到异常时显式释放该锁,或者如果代码块有多个返回点。

内部实现与效率

在此实现中,每个 读写锁 对象包含三个同步对象:

  1. 一个 CriticalSection 对象。此对象需要保护所有其他成员,以便对它们的操作可以原子地完成。在内部,CriticalSection 对象是一块内存(24 字节)加上一个内核事件对象(使用 CreateEvent 函数)。要理解我为什么这么说,请参阅“Windows 下摆脱临界段死锁的困境”。如果临界段代码非常短(进入后很快离开),则几乎从不创建事件对象。这解释了为什么 CriticalSection 是 Win32 中最快的同步方法。查看源代码,您会发现我的实现满足此条件 – 每个临界段内的代码都非常短小。因此,事件对象几乎从不被创建。
  2. 一个手动重置事件对象。当新读取者必须等待直到没有写入者占用 读写锁 对象时,将动态创建此对象。当不再需要时,此事件对象将自动删除以节省系统资源。此事件对象扮演着 .NET 团队称之为“读取队列”的角色。
  3. 一个自动重置事件对象。当新写入者必须等待直到 读写锁 对象未被任何活动线程(读取者或写入者)锁定,将动态创建此对象。当不再需要时,此事件对象也将自动删除以节省系统资源。此事件对象扮演着 .NET 团队称之为“写入队列”的角色。

总而言之,当正确使用这些类时(多个读取者,几乎没有写入者),读写锁 对象几乎不使用任何内核对象,读取线程几乎从不做 WaitForXXX 操作。此算法决定了我的实现的效率:速度快,并且消耗的内核资源非常少。“消耗的内核资源非常少”将是一个巨大的优势,如果您需要在应用程序中同时使用许多 读写锁 对象。

讨论

CReaderWriterLockNonReentrance 类相比,CReaderWriterLock 类更容易使用,终端用户(开发人员)不必像使用非重入版本那样担心死锁。然而,非重入版本更快,内存使用效率更高:它没有哈希表来维护每个线程的计数器,因此避免了动态内存分配的开销。自然,一个常见的问题会出现在我们脑海中:我们应该使用哪一个?这个问题并不容易回答,以下只是我个人的想法:

  1. 如果您的团队成员(同事)在多线程和同步方面花费的时间不够多,那么 CriticalSectionMutex 是减少错误使用风险的好选择。
  2. 如果并发和高速至关重要,有经验的人会偏爱非重入版本(CReaderWriterLockNonReentrance),因为他/她可以控制一切。
  3. 如果并发和高速至关重要,但重入问题非常难以避免,那么 CReaderWriterLock 似乎是必不可少的。在这种情况下,可以考虑将 STL map 类替换为更合适的类,以减少动态内存分配的开销。供您参考,我想介绍 Joaquín M López Muñoz 的文章“用于加速 VC++ STL 的自定义块分配器”,该文章可以在几分钟内集成到此库中,但我将其留给您来保持我的源代码的简洁性。
  4. 如果您不想浪费一个 CriticalSection 对象,请考虑使用自旋锁(Spin Lock)来替换它。

历史

  • 2007 年 10 月 17 日:版本 2.0
    • 支持超时
    • CReaderWriterLockNonReentrance 类中,将方法 ReleaseReaderAndAcquireWriterLock 重命名为 UpgradeToWriterLock
  • 2007 年 1 月 14 日:版本 1.2
    • 修改了一些方法,使临界区内的代码尽可能短
    • CReaderWriterLock 类中添加了更多方便的方法
    • 最后但也是最重要的,CReaderWriterLockNonReentrance 进行了深度修改,使其比以前的版本更能用于实际工作
  • 2006 年 2 月 28 日:版本 1.1
    • 将许可证更改为“宽通用公共许可证”(Lesser General Public License)(感谢 Mitchel Haas
    • 通过重新定义 ASSERTVERIFY 宏,增加了对非 MFC 项目的支持
    • 添加了一些辅助类,CAutoReadLockCAutoWriteLock(感谢 Andy318
    • 修改了演示项目,以显示 CReaderWriterLockNonReentrance 类的一个非预期行为
  • 2006 年 2 月 1 日:版本 1.0
© . All rights reserved.