排队的自旋锁

排队旋转锁是旋转锁 的变体,非常适合高度竞争的锁。 传统的、未排队的旋转锁是一个更好的选择,适合轻竞争或较短的持续时间锁。

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

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

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

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

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

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

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

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,则可以改为调用 KeAcquireInStackQueuedSpinLockAtDpcLevel 和 KeReleaseInStackQueuedSpinLockFromDpcLevel