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






4.87/5 (26投票s)
一个在 C++ 中非常高效的读写锁类,它类似于 .NET 的 ReaderWriterLock
引言
读写锁
(ReaderWriterLock)机制用于同步对资源的访问。在任何给定时间,它允许多个线程并发读取,或者只允许一个线程写入。在资源不经常更改的情况下,读写锁
比简单的逐一锁定(例如 CriticalSection 或 Mutex)能提供更好的吞吐量。
背景
.NET Framework 中的 System.Threading.ReaderWriterLock
类 提供了一种 Win32 没有的新同步方法。如果您从未听说过读写锁的概念,那么 .NET 文档是一个很好的起点。以下是我对 .NET Framework 中实现的 ReaderWriterLock
特性的总结:
ReaderWriterLock
用于同步对资源的访问。在任何给定时间,它允许多个线程并发读取,或者只允许一个线程写入。- 该类在大多数访问都是读取、而写入操作**不频繁**且**持续时间短**的情况下效果最佳。在这种情况下,
ReaderWriterLock
比简单的逐一锁定(如CriticalSection
或Mutex
)能提供更好的吞吐量。 - 当一个写入者正在等待活动读取锁被释放时,请求新读取锁的线程**必须在读取队列中等待**。即使它们可以与现有的读取锁持有者共享并发访问,它们的请求也不会被授予;这有助于保护写入者免受读取者造成的无限期阻塞。
- 支持**重入**(允许同一线程多次调用
AcquireReaderLock
/AcquireWriterLock
)和**锁升级**(在拥有读取锁时调用UpgradeToWriterLock
)。
C++ 读写锁类
我对 读写锁
机制的实现具有 .NET 类的所有特性,但“锁句柄”除外。此外,此实现中有两个类:每个类都有其自身的便利性(当然也有不便之处)。以下是它们重要方法的简要描述:
CReaderWriterLockNonReentrance
AcquireReaderLock |
获取读取锁。调用此方法时,请确保调用线程尚未拥有任何锁(读锁或写锁);否则可能会发生死锁。 |
ReleaseReaderLock |
释放读取锁。调用此方法时,请确保调用线程已拥有读取锁;否则对象的行为将是未定义的。 |
AcquireWriterLock |
获取写入锁。调用此方法时,请确保调用线程尚未拥有任何锁(读锁或写锁);否则可能会发生死锁。 |
ReleaseWriterLock |
释放写入锁。调用此方法时,请确保调用线程已拥有写入锁;否则对象的行为将是未定义的。 |
DowngradeFromWriterLock |
以原子操作释放写入锁并获取读取锁。调用此方法时,请确保调用线程已拥有写入锁;否则对象的行为将是未定义的。 |
UpgradeToWriterLock |
释放读取锁并获取写入锁。如果发生“超时”,此方法将在返回前自动重新获取读取锁。换句话说:如果“升级”因“超时”而失败,调用线程仍将持有(拥有)读取锁。调用此方法时,请确保线程已拥有读取锁;否则对象的行为将是未定义的。 |
本文附带的演示项目还显示了 CReaderWriterLockNonReentrance
类的一个非预期行为(在一个线程中调用 AcquireXXX
,然后在另一个线程中调用 ReleaseXXX
)。虽然不推荐使用此非预期功能,但在某些情况下它非常有用;请谨慎使用。
CReaderWriterLock
AcquireReaderLock |
一个线程可以多次调用 AcquireReaderLock ,每次都会增加读取锁计数器。您必须为每次调用 AcquireReaderLock 调用一次 ReleaseReaderLock 。或者,您可以调用 ReleaseAllLocks 将锁计数立即减少到零。注意:当满足以下两个条件之一时,读取锁请求将始终立即获得: a) 当前线程已拥有写入锁;这是为了防止线程阻塞自身。 b) 当前线程已拥有读取锁(支持递归锁) |
ReleaseReaderLock |
减少读取锁计数器。当计数器达到零时,锁被释放。调用此方法时,请确保调用线程已拥有读取锁;否则将在 DEBUG 模式下引发异常。 |
AcquireWriterLock |
一个线程可以多次调用 |
ReleaseWriterLock |
减少写入锁计数器。当计数器达到零时,锁被释放。调用此方法时,请确保调用线程已拥有写入锁;否则将在 DEBUG 模式下引发异常。 注意:如果对象检测到此请求对应于之前的自动升级,它也将自动降级(释放写入锁但仍保留读取锁)。 |
ReleaseAllLocks |
将调用线程的所有锁计数器(写入计数器和读取计数器)重置为零,并释放写入锁和读取锁,无论该线程拥有多少次读取锁或写入锁。 注意:之后,任何对 ReleaseWriterLock 或 ReleaseReaderLock 的调用将在 DEBUG 模式下引发异常。 |
GetCurrentThreadStatus |
检索调用线程的锁计数器(读取计数器和写入计数器)。 |
正如有些人可能注意到的,没有像 **CReaderWriterLockNonReentrance
** 类那样的“升级”或“降级”方法。实际上,我们不需要这些方法,因为这些操作(“升级”或“降级”)将是隐式和自动进行的。
一些辅助类
CAutoReadLock(T)
和 CAutoWriteLock(T)
允许线程在一个代码块中获取锁,而不必担心在遇到异常时显式释放该锁,或者如果代码块有多个返回点。
内部实现与效率
在此实现中,每个 读写锁
对象包含三个同步对象:
- 一个
CriticalSection
对象。此对象需要保护所有其他成员,以便对它们的操作可以原子地完成。在内部,CriticalSection
对象是一块内存(24 字节)加上一个内核事件对象(使用CreateEvent
函数)。要理解我为什么这么说,请参阅“Windows 下摆脱临界段死锁的困境”。如果临界段代码非常短(进入后很快离开),则几乎从不创建事件对象。这解释了为什么CriticalSection
是 Win32 中最快的同步方法。查看源代码,您会发现我的实现满足此条件 – 每个临界段内的代码都非常短小。因此,事件对象几乎从不被创建。 - 一个手动重置事件对象。当新读取者必须等待直到没有写入者占用
读写锁
对象时,将动态创建此对象。当不再需要时,此事件对象将自动删除以节省系统资源。此事件对象扮演着 .NET 团队称之为“读取队列”的角色。 - 一个自动重置事件对象。当新写入者必须等待直到
读写锁
对象未被任何活动线程(读取者或写入者)锁定,将动态创建此对象。当不再需要时,此事件对象也将自动删除以节省系统资源。此事件对象扮演着 .NET 团队称之为“写入队列”的角色。
总而言之,当正确使用这些类时(多个读取者,几乎没有写入者),读写锁
对象几乎不使用任何内核对象,读取线程几乎从不做 WaitForXXX
操作。此算法决定了我的实现的效率:速度快,并且消耗的内核资源非常少。“消耗的内核资源非常少”将是一个巨大的优势,如果您需要在应用程序中同时使用许多 读写锁
对象。
讨论
与 CReaderWriterLockNonReentrance
类相比,CReaderWriterLock
类更容易使用,终端用户(开发人员)不必像使用非重入版本那样担心死锁。然而,非重入版本更快,内存使用效率更高:它没有哈希表来维护每个线程的计数器,因此避免了动态内存分配的开销。自然,一个常见的问题会出现在我们脑海中:我们应该使用哪一个?这个问题并不容易回答,以下只是我个人的想法:
- 如果您的团队成员(同事)在多线程和同步方面花费的时间不够多,那么
CriticalSection
和Mutex
是减少错误使用风险的好选择。 - 如果并发和高速至关重要,有经验的人会偏爱非重入版本(
CReaderWriterLockNonReentrance
),因为他/她可以控制一切。 - 如果并发和高速至关重要,但重入问题非常难以避免,那么
CReaderWriterLock
似乎是必不可少的。在这种情况下,可以考虑将 STL map 类替换为更合适的类,以减少动态内存分配的开销。供您参考,我想介绍 Joaquín M López Muñoz 的文章“用于加速 VC++ STL 的自定义块分配器”,该文章可以在几分钟内集成到此库中,但我将其留给您来保持我的源代码的简洁性。 - 如果您不想浪费一个
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)
- 通过重新定义
ASSERT
和VERIFY
宏,增加了对非 MFC 项目的支持 - 添加了一些辅助类,
CAutoReadLock
和CAutoWriteLock
(感谢 Andy318) - 修改了演示项目,以显示
CReaderWriterLockNonReentrance
类的一个非预期行为
- 2006 年 2 月 1 日:版本 1.0