排队的自旋锁

排队旋转锁 是旋转锁的一个变体,特别适用于资源竞争非常激烈的锁。 传统的、无队列的旋转锁是轻竞争或短时间锁的更好选择。

使用排队旋转锁的好处包括:

  1. 减少处理器争用:当多个线程同时尝试获取锁时,传统的自旋锁可能会导致严重的处理器争用,因为它们会连续循环(或自旋)检查锁状态。 这可能会降低系统性能,尤其是在多处理器系统上。 队列旋转锁通过将线程组织到队列中来缓解这一问题。 当一个线程获取锁时,只有下一个线程在积极自旋,等待获取该锁。 这减少了旋转时浪费的 CPU 周期,尤其是在锁被持有较长时间时。

  2. 公平和避免饥饿:基本旋转锁的问题之一是缺乏公平性:如果其他线程持续获取并释放锁,则线程可能会饿死,并且永远不会获取锁。 队列化的旋转锁通过确保线程按照尝试的顺序获取锁来解决此问题。 此顺序处理可防止饥饿,并确保所有线程在一段时间内得到服务。

  3. 可伸缩性:随着系统中处理器或核心数的增加,同步机制的效率对于性能至关重要。 排队的旋转锁比传统的旋转锁更具可伸缩性,因为它们在所有核心中最小化了活跃自旋,从而减少了处理器上的开销。 这在高性能、多核系统中尤其重要,在这些系统中,驱动程序效率直接影响到整体系统性能。

  4. 有效使用系统资源:通过减少不必要的处理器旋转,排队旋转锁使系统能够更有效地使用其资源。 这不仅提高了设备驱动程序的性能,而且对系统的整体响应能力和能耗产生了积极影响,这对对电源敏感型环境尤其有利。

  5. 简单性与可靠性:尽管它们在减少争用和提高公平性方面具有优势,但排队的旋转锁将复杂性从开发人员的职责中抽象出来。 它们提供一种简单可靠的机制来保护共享资源,而无需开发人员实现复杂的锁定逻辑。 这种简单性可降低与错误锁处理相关的 bug 的可能性,从而提高驱动程序的可靠性。

下面是一个简化的代码示例,演示了在 Windows 内核模式驱动程序中使用排队自旋锁的上述操作。 此示例演示如何使用 KeInitializeSpinLock 声明和初始化旋转锁,然后分别使用 KeAcquireInStackQueuedSpinLockKeReleaseInStackQueuedSpinLock 获取和释放锁。

KSPIN_LOCK SpinLock;  
KLOCK_QUEUE_HANDLE LockHandle;  

// Initialize the spin lock  
KeInitializeSpinLock(&SpinLock);  

// Assume this function is called in some kind of context where   
// the below operations make sense, e.g., in a device I/O path  

// Acquire the queued spin lock  
KeAcquireInStackQueuedSpinLock(&SpinLock, &LockHandle);  

// At this point, the current thread holds the spin lock.  
// Perform thread-safe operations here.  
    
// ...  

// Release the queued spin lock  
KeReleaseInStackQueuedSpinLock(&LockHandle);  

驱动程序分配一个 KLOCK_QUEUE_HANDLE 结构,并通过指针将其传递给 KeAcquireInStackQueuedSpinLock。 驱动程序在释放旋转锁时,通过指向KeReleaseInStackQueuedSpinLock的指针传递相同的结构。

驱动程序每次获取锁时,通常会在堆栈上分配结构。 驱动程序不应在其设备上下文中分配结构,然后从多个线程共享相同的结构。

驱动程序不得将对排队旋转锁例程的调用与同一旋转锁上的普通 KeXxxSpinLock 例程混合使用。

如果驱动程序已在 IRQL = DISPATCH_LEVEL,则可以改为调用 KeAcquireInStackQueuedSpinLockAtDpcLevelKeReleaseInStackQueuedSpinLockFromDpcLevel