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
}