跨进程读写锁开发(第三部分):公平性设计Developing a cross-process reader/writer lock with limited readers, part 3: Fairness
微软资深工程师 Raymond Chen 在其博客中继续探讨跨进程读写锁的实现,本部分重点讨论如何确保独占锁获取与共享锁获取之间的公平性。文章分析了现有机制中的潜在不公平问题,并提出改进方案以实现更均衡的并发控制。
Raymond Chen
我们一直在构建一个跨进程的读写锁,并且对读者数量设置了上限。上次调查结束时,我们注意到独占访问的吞吐量很差。这到底是怎么回事?
问题在于,独占获取操作是逐个申请信号量令牌的,因此即使独占请求已经开始,它仍可能被共享请求超越——共享请求可以“插队”到独占请求之前,从而导致独占请求被饿死。
假设我们将共享获取的数量限制为五个。在上述场景中,有一个正在尝试进行独占获取的线程,以及四个正在尝试进行共享获取的线程。前两个共享获取线程(我们称它们为 A 和 B)成功获得了共享锁,接着独占获取线程试图进入独占模式。独占线程需要五个令牌,它在尝试获取第四个令牌时很快获得了三个,然后被阻塞。
当独占获取线程等待获取其第四个令牌时,另外两个共享获取线程(我们称它们为 C 和 D)也尝试以共享模式进入。它们同样被阻塞。
其中一个原始共享获取线程释放了其共享锁,释放出一个令牌,该令牌迅速被独占获取线程获得,这得益于同步对象的“基本先进先出”策略。(为了便于讨论,我们假设没有发生任何破坏先进先出特性的情况。)现在,独占获取线程等待获取其第五个令牌。
当第二个原始共享获取线程释放其令牌时,该令牌被分配给线程 C,尽管线程 C 是在独占获取线程尝试独占获取之后才开始其共享获取的。
然后,当线程 C 释放其令牌时,该令牌被分配给线程 D,因为其对该令牌的需求在独占线程对其第五个令牌的需求之前。独占获取线程再次被排挤在外。
要解决这个问题,我们可以让所有获取操作都申请共享互斥锁。这样,共享互斥锁就能负责强制执行所有获取操作的“基本先进先出”行为。
由于我们将使用组合超时机制,我将把超时管理重构为一个辅助类。
struct TimeoutTracker
{
explicit TimeoutTracker(DWORD timeout)
: m_timeout(timeout) {}
DWORD m_start = GetTickCount();
bool Wait(HANDLE h)
{
DWORD elapsed = GetTickCount() - m_start;
if (elapsed > m_timeout) return false;
return WaitForSingleObject(h, m_timeout - elapsed)
== WAIT_OBJECT_0;
}
};我们可以使用这个辅助类来管理我们的超时。
HANDLE sharedSemaphore;
HANDLE sharedMutex;
void AcquireShared()
{
WaitForSingleObject(sharedMutex, INFINITE);
WaitForSingleObject(sharedSemaphore, INFINITE);
ReleaseMutex(sharedMutex);
}
bool AcquireSharedWithTimeout(DWORD timeout)
{
TimeoutTracker tracker(timeout);
bool result = tracker.Wait(hSharedMutex);
if (!result) return false;
result = tracker.Wait(sharedSemaphore);
ReleaseMutex(sharedMutex);
return result;
}
// no change to AcquireExclusive
void AcquireExclusive()
{
WaitForSingleObject(sharedMutex, INFINITE);
for (unsigned i = 0; i < MAX_SHARED; i++) {
WaitForSingleObject(sharedSemaphore, INFINITE);
}
ReleaseMutex(sharedMutex);
}
// no functional change, but using the new helper class
bool AcquireExclusiveWithTimeout(DWORD timeout)
{
TimeoutTracker tracker(timeout);
bool result = tracker.Wait(sharedMutex);
if (!result) return false;
for (unsigned i = 0; i < MAX_SHARED; i++) {
if (!tracker.Wait(sharedSemaphore)) {
// Restore the tokens we already claimed.
if (i > 0) {
ReleaseSemaphore(sharedSemaphore, i, nullptr);
}
ReleaseMutex(sharedMutex);
return false;
}
}
ReleaseMutex(sharedMutex);
return true;
}(是的,我没有使用 RAII。出于教学目的,我做出了这个选择,因为它能让你清楚地看到每个同步对象何时被获取和释放。)
我们完成了吗?
不,我们还没有完成。
仍然有一个严重的问题需要修复。下次我们会讨论它。
作者
Raymond 参与 Windows 系统演化已超过 30 年。2003 年,他创建了一个名为 The Old New Thing 的网站,其受欢迎程度远超他的想象,这一发展至今仍让他感到有些不安。该网站催生了一本同名书籍《The Old New Thing》(Addison Wesley, 2007)。他偶尔会在 Windows Dev Docs 的 Twitter 账号上出现,讲述一些毫无实用价值的故事。
需要完整排版与评论请前往来源站点阅读。