如何使 Windows slim 读写锁公平?

Posted

技术标签:

【中文标题】如何使 Windows slim 读写锁公平?【英文标题】:How to make windows slim read writer lock fair? 【发布时间】:2016-10-13 15:50:22 【问题描述】:

我发现 Windows 实现了一个纤细的读写器锁(参见 https://msdn.microsoft.com/en-us/library/windows/desktop/aa904937%28v=vs.85%29.aspx )。不幸的是(对我来说)这个 rw-lock 既不是先进先出也不公平(在任何意义上)。 是否有可能通过一些解决方法公平或先进先出使 Windows rw-lock? 如果没有,你会在哪些场景下使用 windows slim rw-lock?

【问题讨论】:

【参考方案1】:

您不太可能将细长锁本身更改为公平,尤其是因为文档没有说明这样做的任何方法,而且出于性能原因,今天大多数锁不公平。 p>

也就是说,使用 Windows events 滚动您自己的近似 FIFO 锁是相当简单的,而您使用 compare and swap 操作的 64 位控制字仍然非常小。这是一个大纲:

锁的状态反映在控制字被原子操作以在状态之间转换,并允许线程通过单个原子操作进入锁(如果允许)而无需内核切换(这是“苗条的”)。重置事件用于通知等待线程,当线程需要阻塞并且可以按需分配(这是 slim 的低内存占用)。

锁定控制字有以下几种状态:

    免费 - 没有读者或作者,也没有服务员。任何线程都可以通过原子方式将锁转换为状态 (2) 或 (3) 来获取用于读取或写入的锁。

    锁中有 N 个读卡器。目前锁中有 N 个读者。新读者可以通过将计数加 1 来立即获得锁——在控制字中使用一个 30 位左右的字段来表示这个计数。作家必须阻止(也许在旋转之后)。当读者离开锁时,他们会减少计数,当最后一个读者离开时,它可能会转换到状态 (1)(尽管他们不需要在 (2) -> (1) 转换中做任何特殊的事情)。

    状态 (2) + 等待写入者 + 0 个或更多等待读取者。在这种状态下,还有 1 个或多个读者仍处于锁中,但至少有一个正在等待的写者。编写器应等待手动重置事件,该事件被设计为(尽管不能保证)为 FIFO。控制字中有一个字段来指示有多少写入器正在等待。在这种状态下,想要进入锁的新读者不能,而是设置一个读者等待位,并阻止读者等待事件。新写入者增加等待写入者计数并阻止写入者等待事件。当最后一个 reader 离开时(设置 reader-count 字段为 0),它signals writer-waiting 事件,释放等待时间最长的 writer 进入锁。

    锁中的写入器。当写入者处于锁定状态时,所有读取者都会排队等待读取者等待事件。所有传入的写入者都会增加等待写入者的计数,并像往常一样在写入者等待事件中排队。由于上面的状态(3),写入者在获取锁时甚至可能有一些等待的读取者,这些被同等对待。当写入者离开锁时,它会检查等待中的写入者和读取者,并根据策略取消阻止写入者或所有读取者,如下所述。

上面讨论的所有状态转换都是使用比较和交换以原子方式完成的。典型的模式是任何lock()unlock() 调用都会查看控制字,确定它们处于什么状态以及需要发生什么转换(遵循上面的规则),然后临时计算新的控制字尝试使用比较和交换来交换新的控制字。有时该尝试会失败,因为另一个线程同时修改了控制字(例如,另一个读取器进入了锁,增加了读取器计数)。没问题,从“确定状态...”重新开始,然后再试一次。这种竞争在实践中很少见,因为状态字计算非常短,而这正是基于 CAS 的复杂锁的工作原理。

这种锁设计“纤薄”几乎是每一种感觉。在性能方面,它接近通用设计所能达到的最高水平。特别是,常见的快速路径(a)读取器进入锁,0 个或多个读取器已经在块中(b)读取器离开锁,0 或多个读取器仍在锁中,(c)写入器进入/离开一个在通常情况下,非竞争锁都尽可能快:单个原子操作。此外,读卡器进入和退出路径是“无锁”的,因为传入的读卡器不会临时获取 rwlock 内部的互斥锁,操纵状态,然后在进入/离开锁时解锁它。这种方法很慢,并且每当读取器线程在持有内部锁的关键时刻执行上下文切换时都会出现问题。这种方法不能扩展到具有较短 rwlock 临界区的大量阅读器活动:即使理论上多个阅读器可以进入临界区,但它们在进入和离开内部锁时都会成为瓶颈(每次进入/退出操作都会发生两次) ) 并且性能比普通的互斥体差!

它也是轻量级的,因为它只需要几个 Windows Event 对象,并且这些对象可以按需分配 - 它们仅在发生争用并且需要阻塞的状态转换时才需要即将发生。这就是CRITICAL_SECTION 对象的工作方式。

上面的锁是公平的,因为读者不会阻塞写者,写者是按照先进先出的顺序服务的。作家如何与等待的读者互动取决于您的政策,即当作家解锁后锁变为空闲并且有 等待的读者和作家时,谁来解除阻塞。一个简单的策略是取消阻止所有等待的读者。

在此设计中,写入器将按 FIFO 顺序与 FIFO 批次的读取器交替。写入器相对于其他写入器是 FIFO,读取器批次相对于其他读取器批次是 FIFO,但写入器和读取器之间的关系并不是 完全 FIFO:因为所有传入的读取器都添加到同一个读取器 -等待集,在已经有几个等待写入者的情况下,到达的读取者都进入下一个要发布的“批次”,这实际上将它们排在已经等待的写入者之前。不过这很合理:阅读器都同时,因此向批处理添加更多阅读器并不需要太多成本,并且可能会提高效率,如果您确实以严格的 FIFO 顺序为所有线程提供服务,那么锁将在竞争下将行为减少为简单的互斥体。

另一种可能的设计是,如果有正在等待的写入器,则始终解除阻止。这以牺牲读者为代价有利于作家,并且确实意味着永无止境的作家流可能会无限期地阻止读者。如果您知道写入对延迟敏感很重要,并且您不担心读取器饥饿,或者您知道由于应用程序的设计而不会发生这种情况(例如,因为只有一个可能的写入器在一次)。

除此之外,还有许多其他可能的策略,例如在读者等待一段时间之前偏爱作者,或者限制读者批量大小,等等。它们大多可以有效地实现,因为簿记通常仅限于线程无论如何都会阻塞的慢速路径。

我在这里略过了一些实现细节和陷阱(特别是在进行涉及阻塞的转换时需要小心以避免“错过唤醒”问题)——但这绝对有效。在超薄 rwlock 出现之前,我已经编写了这样一个锁来满足对快速高性能 rwlock 的需求,并且它的性能非常好。其他权衡也是可能的,例如,对于预期读取占主导地位的设计,可以通过在高速缓存行之间拆分控制字来减少争用,但代价是更昂贵的写入操作。

最后一点 - 在内存使用方面,这个锁比在竞争情况下的 Windows 锁要胖一些 - 因为它为每个锁分配一个或两个窗口事件,而 slim lock 避免了这种情况。 slim lock 很可能是通过直接支持内核中的 slim lock 行为来实现的,因此控制字可以直接用作内核端等待列表的一部分。您无法准确地重现这一点,但您仍然可以通过另一种方式消除每个锁的开销:使用线程本地存储来分配您的两个事件每个线程,而不是每个锁。由于一个线程一次只能等待一个锁,因此每个线程只需要一个这个结构。这使它与内存使用中的小锁保持一致(除非你有很少的锁和大量的线程)。

【讨论】:

【参考方案2】:

这个 rw-lock 既不先进也不公平

我不希望线程是“公平”或“先进先出”,除非它明确表示。在这种情况下,我希望写锁优先,因为如果有很多读取线程,它可能永远无法获得锁,但我也不会假设是这种情况,我没有读过MS 文档以便更好地理解。

也就是说,听起来您的问题是您对锁有很多争用,这是由写入线程引起的;因为否则您的读者将始终能够共享锁。您可能应该检查为什么您的编写线程试图保持锁定这么长时间;例如,缓冲添加将有助于缓解这种情况。

【讨论】:

以上是关于如何使 Windows slim 读写锁公平?的主要内容,如果未能解决你的问题,请参考以下文章

可重入锁 公平锁 读写锁

3.1.4 读写锁

锁-概念:可重入锁可中断锁公平锁读写锁

JUC - 多线程之悲观锁乐观锁,读写锁(共享锁独享锁),公平非公平锁,可重入锁,自旋锁,死锁

Java实现锁公平锁读写锁信号量阻塞队列线程池等常用并发工具

Java锁机制:乐观锁 悲观锁 自旋锁 可重入锁 读写锁 公平锁 非公平锁 共享锁 独占锁 重量级锁 轻量级锁 偏向锁 分段锁 互斥锁 同步锁 死锁 锁粗化 锁消除