Windows Programming/读写锁

维基教科书,自由的教学读本

读写锁(Slim Read Write Lock, SRW)是自Windows Vista 引入的线程同步用的轻量级工具。从React OS的实现看,SRW具有下述特点:

  • 适用于一个进程内部各线程的读操作、写操作同步
  • 各线程采用自旋锁忙等待

SRW Lock是一个4字节长的指针。它的低4位被用于4个不同的标志,对应的宏定义:

#define RTL_SRWLOCK_OWNED_BIT   0       //是否有线程正占有资源
#define RTL_SRWLOCK_CONTENDED_BIT   1   //是否有线程在等待 
#define RTL_SRWLOCK_SHARED_BIT  2       //是否有线程正在读取 
#define RTL_SRWLOCK_CONTENTION_LOCK_BIT 3  //作为线程等待块(WaitBlock)链表的表头指针修改时的自旋锁

#define RTL_SRWLOCK_OWNED   (1 << RTL_SRWLOCK_OWNED_BIT) //第0位为1表示有线程正在读/写共享资源  
#define RTL_SRWLOCK_CONTENDED   (1 << RTL_SRWLOCK_CONTENDED_BIT) //第1位为1表示一个或者多个生产线程在等待独占资源  
#define RTL_SRWLOCK_SHARED  (1 << RTL_SRWLOCK_SHARED_BIT)//第2位为1表示一个或者多个消费线程正在读取  
#define RTL_SRWLOCK_CONTENTION_LOCK (1 << RTL_SRWLOCK_CONTENTION_LOCK_BIT) //第3位为1标识有一个线程在修改WAITBLOCK结构的表头指针  

#define RTL_SRWLOCK_MASK    (RTL_SRWLOCK_OWNED | RTL_SRWLOCK_CONTENDED | \  
	RTL_SRWLOCK_SHARED | RTL_SRWLOCK_CONTENTION_LOCK)
#define RTL_SRWLOCK_BITS    4

SRW Lock的高28位的作用:

  • 只有读操作线程时,则是所有读操作线程的数量;
  • 有写操作线程时,是线程等待块(Wait Block)链表的表头指针。因为它所指向的内存块的对齐方式是16个字节,即地址的最低4位都是0。 写/读线程分别调用AcquireSRWLockExclusive或者AcquireSRWLockShared时,会将一个在线程栈上构建的结构体挂入SRW Lock所指向的链表。

等待块的结构体定义:

typedef struct _RTLP_SRWLOCK_WAITBLOCK
{
 LONG SharedCount; //有多少线程 在等待读取  
 volatile struct _RTLP_SRWLOCK_WAITBLOCK *Last;
 volatile struct _RTLP_SRWLOCK_WAITBLOCK *Next;   //链表节指针  

 union
 {
    LONG Wake;                                   //非0表示可以被唤醒,0表示继续睡眠  
    struct
    {
       PRTLP_SRWLOCK_SHARED_WAKE SharedWakeChain;   //需要被唤醒的消费线程链表  
       PRTLP_SRWLOCK_SHARED_WAKE LastSharedWake;//最后一个要被唤醒的读操作线程。如果多个读操作线程等待,形成一个链表  
    };
 };
 BOOLEAN Exclusive;              //1表示该结构体对象由生产线程构建在栈上,0表示结构体由消费线程构建在栈上  
} volatile RTLP_SRWLOCK_WAITBLOCK, *PRTLP_SRWLOCK_WAITBLOCK;

typedef struct _RTLP_SRWLOCK_SHARED_WAKE //单向链表结构 ,代表每个需要被唤醒的读线程
{
	LONG Wake;   //唤醒标志,非0唤醒,0睡眠  
	volatile struct _RTLP_SRWLOCK_SHARED_WAKE *Next;
} volatile RTLP_SRWLOCK_SHARED_WAKE, *PRTLP_SRWLOCK_SHARED_WAKE;

总结状态-动作表(有限状态自动机):

读写锁状态 读线程申请锁 读线程释放锁 写线程申请锁 写线程释放锁 注释
占用与等待线程均为空 高24位共享计数置1,置位RTL_SRWLOCK_SHARED与RTL_SRWLOCK_OWNED 不可能 置位RTL_SRWLOCK_OWNED 不可能 初始化状态
读线程占用 无等待线程 高24位共享计数加1,保持RTL_SRWLOCK_SHARED 与RTL_SRWLOCK_OWNED置位 高24位共享计数减1,不为0则保持SHARED与OWND置位 栈上的StackWaitBlock 成为第一个等待块,置位 RTL_SRWLOCK_SHARED,RTL_SRWLOCK_OWNED与 RTL_SRWLOCK_CONTENDED 不可能 示例
有线程等待,第一个等待线程是写线程 最后一个等待线程是写线程 栈上的StackWaitBlock 加入等待线程列表的尾部 共享计数减1后如果为0,则等待块链表表头指向第二个等待块或置空,原来的第一个等待块Wake标志置位 栈上的StackWaitBlock 加入等待线程列表的尾部 不可能 示例
最后一个等待线程是读线程 栈上的SharedWake 块加入最后一个等待块的读线程链表的尾部,最后一个等待块的共享计数加1 不可能 示例
有线程等待,第一个等待线程是读线程 栈上的SharedWake 块加入第一个等待块的读线程链表的尾部,第一个等待块的共享计数加1 不可能 不可能 示例
写线程占用 无等待线程 栈上的SharedWake 块成为第一个等待块,置位RTL_SRWLOCK_OWNED 与 RTL_SRWLOCK_CONTENDED 不可能 栈上的StackWaitBlock 成为第一个等待块,置位 RTL_SRWLOCK_SHARED,RTL_SRWLOCK_OWNED与 RTL_SRWLOCK_CONTENDED 置为全0 示例
有等待线程 最后等待线程是写线程 栈上的StackWaitBlock 加入等待线程列表的尾部 不可能 栈上的StackWaitBlock 加入等待线程列表的尾部 调整等待块表头指针指向第二个等待块或为空;原来第一个等待块如果为写操作则Wake标志置位,如果为读操作则SharedBlock链表的每一个块Wake标志置位 示例
最后等待线程是读线程 栈上的SharedWake 块加入最后一个等待块的读线程链表的尾部,最后一个等待块的共享计数加1 不可能 示例

下面是各API的具体实现:

VOID NTAPI RtlInitializeSRWLock(OUT PRTL_SRWLOCK SRWLock)
{
    SRWLock->Ptr = NULL;   //直接赋值为0
}