返回 2026-05-01
⚙️ 工程

跨进程读写锁开发(第三部分):公平性设计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 账号上出现,讲述一些毫无实用价值的故事。

需要完整排版与评论请前往来源站点阅读。