Windows核心编程笔记(八)用户模式下的线程同步 SRWLock剖析

在VISTA及之后的系统中,引入了 SRWLock 用户用户模式的线程同步,MSDN中是这样描述的。




SRWLock  轻量级的读写锁,它与临界区对象的不同在于,它分为两个模式来访问共享资源。并假设有两种类型的线程同时工作,一种用在读取共享资源,通常又称为消费线程,另一种用来对共享的资源进程写入操作,通常又称为生产线程。


共享模式下每个读取线程可以读取共享的数据,不受任何限制,它们可以同时读取共享数据。


独占模式下只运行一个写入线程访问共享数据,其他读/写线程都会被堵塞。 直到该线程释放了读写锁。


在多线程的情况下,生产线程和消费线程是无序的,SRWLock是一个指针,它的优势在于可以快速的更新锁的状态,劣势在这个指针可以存放有限的信息,因此SRWLock不能够递归获取,此外一个线程处于共享模式的时候,不能讲自身升级为独占模式。



初始化读写锁很简单

VOID WINAPI InitializeSRWLock(  PSRWLOCK SRWLock);
在Winbase.h文件中

typedef RTL_SRWLOCK SRWLOCK, *PSRWLOCK;
typedef struct _RTL_SRWLOCK {
  PVOID Ptr;
} RTL_SRWLOCK, *PRTL_SRWLOCK;


可见读写锁确实只是一个指针变量。


使用读写锁:


在消费线程中

AcquireSRWLockShared

*******读取操作

ReleaseSRWLockShared



在生产线程中

AcquireSRWLockExclusive

*******写操作

ReleaseSRWLockExclusive




这个读写锁和关键段一样方便使用,作者经过测试发现读写锁的性能较关键有很大的提升,这是因为读写锁是基于原子访问的,关键段是基于事件内核对象的,从用户模式到内核模式的切换占用了大量的时钟周期。



剖析SRWLock,以React OS下对读写锁的实现代码来看其工作原理。


在实现中SRWLock 的这个指针变量是这样来标识信息的





该指针的低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
#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

SRWLock 的高28位是一个指针,为什么只有28位呢,是因为它要指向对象的对齐方式是16,即地址的最低4位都是0,比如都是这样的0x00124710,0x00124720,0x00124850,   当需要获取指针所指向的对象时,只要将低4位全部置0,就可以得到对象的地址。


SRWLock 在同时有一个以上线程读写资源的时候,它指向一个结构体链表。


在有其他线程在读/资源的时候,一个线程调用AcquireSRWLockExclusive或者AcquireSRWLockShared  时会将一个在栈上构建的结构体挂入SRWLock 所指向的链表,这样每个将要读/写资源的线程都会在栈上构建这么一个结构体,并将结构体挂入链表中。

该结构体是这样定义的


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;




初始化读写锁,只是简单的将指针置为0


NTAPI
RtlInitializeSRWLock(OUT PRTL_SRWLOCK SRWLock)
{
    SRWLock->Ptr = NULL;
}




AcquireSRWLockExclusive


它对应的函数是RtlAcquireSRWLockExclusive(IN OUT PRTL_SRWLOCK SRWLock), 由于这个函数的代码比较长,这里先贴出大体结构,然后分部分注释。


NTAPI
RtlAcquireSRWLockExclusive(IN OUT PRTL_SRWLOCK SRWLock)
{
    __ALIGNED(16) RTLP_SRWLOCK_WAITBLOCK StackWaitBlock;    //构建于栈上的 对齐为16字节的 等待块
    PRTLP_SRWLOCK_WAITBLOCK First, Last;

    if (InterlockedBitTestAndSetPointer(&SRWLock->Ptr,
                                        RTL_SRWLOCK_OWNED_BIT))   //如果有其他线程在访问资源,进入循环,该原子访问函数返回之前的位值
    {
        LONG_PTR CurrentValue, NewValue;

        while (1)
        {
            CurrentValue = *(volatile LONG_PTR *)&SRWLock->Ptr;

            if (CurrentValue & RTL_SRWLOCK_SHARED)
            {
                    if (CurrentValue & RTL_SRWLOCK_CONTENDED)
                {
                    goto AddWaitBlock;
                }    
                else
                {
                }               //Part1, 如果有线程在读取资源,且没有其他生产线程在独占资源
            }
            else
            {
                if (CurrentValue & RTL_SRWLOCK_OWNED)
                {

                    if (CurrentValue & RTL_SRWLOCK_CONTENDED)
                    {
AddWaitBlock:
                                 //Part2,如果有其他线程在等待独占资源                    
                    }
                    else
                    {
				//Part3,如果有线程独占资源,且没有其他线程在等待资源
                    }
                }
                else
                {
                    if (!InterlockedBitTestAndSetPointer(&SRWLock->Ptr,
                                                         RTL_SRWLOCK_OWNED_BIT))
                    {
                        break;
                    }
                }
            }

            YieldProcessor();     执行空指令
        }
    }
}


最后一个函数  只是执行一个NOP指令,只是用来做延时而已,

    #define YieldProcessor() __asm__ __volatile__("nop");




Part1

                if (CurrentValue & RTL_SRWLOCK_CONTENDED)
                {
                    goto AddWaitBlock;
                }
                else
                {
                    StackWaitBlock.Exclusive = TRUE;                               //标识结构体位于生产线程的栈上
                    StackWaitBlock.SharedCount = (LONG)(CurrentValue >> RTL_SRWLOCK_BITS);  //目前的高28位标识多少个线程在等待读取
                    StackWaitBlock.Next = NULL;
                    StackWaitBlock.Last = &StackWaitBlock;
                    StackWaitBlock.Wake = 0;             //初始化这个栈上的结构体
                    NewValue = (ULONG_PTR)&StackWaitBlock | RTL_SRWLOCK_SHARED | RTL_SRWLOCK_CONTENDED | RTL_SRWLOCK_OWNED;//设置新指针为该栈上结构地址
																并设置标志位
                    if ((LONG_PTR)InterlockedCompareExchangePointer(&SRWLock->Ptr,
                                                                    (PVOID)NewValue,
                                                                    (PVOID)CurrentValue) == CurrentValue) 更换指针为新的值
                    {
                        RtlpAcquireSRWLockExclusiveWait(SRWLock,
                                                        &StackWaitBlock);    进入生产线程的等待函数
                        break;
                    }
                


看下这个生产线程的等待函数


NTAPI
RtlpAcquireSRWLockExclusiveWait(IN OUT PRTL_SRWLOCK SRWLock,
                                IN PRTLP_SRWLOCK_WAITBLOCK WaitBlock)
{
    LONG_PTR CurrentValue;

    while (1)
    {
        CurrentValue = *(volatile LONG_PTR *)&SRWLock->Ptr;
        if (!(CurrentValue & RTL_SRWLOCK_SHARED))
        {
            if (CurrentValue & RTL_SRWLOCK_CONTENDED)
            {
                if (WaitBlock->Wake != 0)
                {
                    break;
                }
            }
            else
            {
                break;
            }
        }

        YieldProcessor();                         //只有在没有线程在读取,没有其他生产线程在独占,或者独占的线程将该线程的WAKE标志设为非0,时退出死循环
    }
}

该函数保证了没个生产线程以独占的模式运行,其他生产线程只能在这里死循环,只要被唤醒,或者独占标志位被清除。



Part2:


AddWaitBlock:
                        StackWaitBlock.Exclusive = TRUE;
                        StackWaitBlock.SharedCount = 0;
                        StackWaitBlock.Next = NULL;
                        StackWaitBlock.Last = &StackWaitBlock;
                        StackWaitBlock.Wake = 0;                             初始化结构体

                        First = RtlpAcquireWaitBlockLock(SRWLock);           根据28位的指针去掉标志位,获取指向的链表表头,
                        if (First != NULL)
                        {
                            Last = First->Last;
                            Last->Next = &StackWaitBlock;
                            First->Last = &StackWaitBlock;           //将该线程栈上的结构体挂入链表的最后面,将表头的Last指向最后这个挂入的

                            RtlpReleaseWaitBlockLock(SRWLock);

                            RtlpAcquireSRWLockExclusiveWait(SRWLock,
                                                            &StackWaitBlock);  //进入等待循环

                            break;
                        }

RtlpAcquireWaitBlockLock  揭示了 如何通过28位的指针工作


NTAPI
RtlpAcquireWaitBlockLock(IN OUT PRTL_SRWLOCK SRWLock)
{
    LONG_PTR PrevValue;
    PRTLP_SRWLOCK_WAITBLOCK WaitBlock;

    while (1)
    {
        PrevValue = InterlockedOrPointer(&SRWLock->Ptr,
                                         RTL_SRWLOCK_CONTENTION_LOCK); //低位 第3位如果是1表示,有其他线程在调用该函数根据指针获取链表头
										这里保证了 只有一个线程可以访问链表,其他需要访问的要等待
        if (!(PrevValue & RTL_SRWLOCK_CONTENTION_LOCK))
            break;

        YieldProcessor();
    }

    if (!(PrevValue & RTL_SRWLOCK_CONTENDED) ||
        (PrevValue & ~RTL_SRWLOCK_MASK) == 0)
    {
        RtlpReleaseWaitBlockLock(SRWLock);   //如果现在没有处于独占模式 或者 高28位 为0   ,错误 释放RTL_SRWLOCK_CONTENTION_LOCK标志位
        return NULL;
    }

    WaitBlock = (PRTLP_SRWLOCK_WAITBLOCK)(PrevValue & ~RTL_SRWLOCK_MASK);   将指针与0x11111110  与操作, 将低4位标志清除,构成实际地址

    return WaitBlock;
}

NTAPI
RtlpReleaseWaitBlockLock(IN OUT PRTL_SRWLOCK SRWLock)
{
    InterlockedAndPointer(&SRWLock->Ptr,
                          ~RTL_SRWLOCK_CONTENTION_LOCK);     只是简单将该标志位设为0
}


Part3:


                        StackWaitBlock.Exclusive = TRUE;
                        StackWaitBlock.SharedCount = 0;
                        StackWaitBlock.Next = NULL;
                        StackWaitBlock.Last = &StackWaitBlock;
                        StackWaitBlock.Wake = 0;  初始化结构体

                        NewValue = (ULONG_PTR)&StackWaitBlock | RTL_SRWLOCK_OWNED | RTL_SRWLOCK_CONTENDED;   将结构体地址构成新的指针
                        if ((LONG_PTR)InterlockedCompareExchangePointer(&SRWLock->Ptr,
                                                                        (PVOID)NewValue,
                                                                        (PVOID)CurrentValue) == CurrentValue)  替换原来的指针
                        {
                            RtlpAcquireSRWLockExclusiveWait(SRWLock,
                                                            &StackWaitBlock);   进入等待
                            break;
                        }


第一部分 和第三不分 代码差不多的,唯一的区别就是 第一不分多设置了一个标志位 RTL_SRWLOCK_SHARED,它们都是初始化栈上的结构体 并将结构体的地址加上标志位,然后替换为指针的值。


AcquireSRWLockExclusive  总结:


1、当有线程还在读取时,但是没有被其它生产线程独占, 那么挂入栈等待块,设置标志 (读取  独占 拥有),进入等待,读取线程全部Release时 线程等待结束,以独占模式访问资源

2、有其他生产线程在等待独占时,将栈块挂入SRWLock指针所指向链表的末尾,进入等待,在前面所有已挂入的等待都Release时,线程才结束等待 以独占模式访问资源。

3、如果有线程在独占,但是没有其他在等待独占的,那么挂入栈等待块,设置标志 (独占 拥有),进入等待,读取线程全部Release时 线程等待结束,以独占模式访问资源






ReleaseSRWLockExclusive


NTAPI
RtlReleaseSRWLockExclusive(IN OUT PRTL_SRWLOCK SRWLock)
{
    LONG_PTR CurrentValue, NewValue;
    PRTLP_SRWLOCK_WAITBLOCK WaitBlock;

    while (1)
    {
        CurrentValue = *(volatile LONG_PTR *)&SRWLock->Ptr;

        if (!(CurrentValue & RTL_SRWLOCK_OWNED))   //此时如果不处于拥有状态,抛出异常
        {
            RtlRaiseStatus(STATUS_RESOURCE_NOT_OWNED);
        }

        if (!(CurrentValue & RTL_SRWLOCK_SHARED))  //必须不处于读取状态
        {
            if (CurrentValue & RTL_SRWLOCK_CONTENDED) 
            {
                WaitBlock = RtlpAcquireWaitBlockLock(SRWLock);        
                if (WaitBlock != NULL)
                {
                    RtlpReleaseWaitBlockLockExclusive(SRWLock,
                                                      WaitBlock);           如果有等待独占的线程,调用该函数,将指针指向的链表头传给它

                    break;
                }
            }
            else
            {

                ASSERT(!(CurrentValue & ~RTL_SRWLOCK_OWNED));

                NewValue = 0;
                if ((LONG_PTR)InterlockedCompareExchangePointer(&SRWLock->Ptr,
                                                                (PVOID)NewValue,
                                                                (PVOID)CurrentValue) == CurrentValue)  //如果没有等待独占的,指针置为0
                {
                    break;
                }
            }
        }
        else
        {
            RtlRaiseStatus(STATUS_RESOURCE_NOT_OWNED);             如果处于读取状态 抛出异常
        }

        YieldProcessor();
    }
}



RtlpReleaseWaitBlockLockExclusive:


NTAPI
RtlpReleaseWaitBlockLockExclusive(IN OUT PRTL_SRWLOCK SRWLock,
                                  IN PRTLP_SRWLOCK_WAITBLOCK FirstWaitBlock)
{
    PRTLP_SRWLOCK_WAITBLOCK Next;
    LONG_PTR NewValue;


    Next = FirstWaitBlock->Next;
    if (Next != NULL)  如果还有其他在等待的线程
    {
        NewValue = (LONG_PTR)Next | RTL_SRWLOCK_OWNED | RTL_SRWLOCK_CONTENDED;
        if (!FirstWaitBlock->Exclusive)        如果第一个等待的线程是读取线程
        {
            ASSERT(Next->Exclusive);
            Next->SharedCount = FirstWaitBlock->SharedCount;  //复制读取线程计数

            NewValue |= RTL_SRWLOCK_SHARED;
        }

        Next->Last = FirstWaitBlock->Last;    将Last指向链尾的对象
    }
    else     //如果只有一个在等待的线程
    {
        if (FirstWaitBlock->Exclusive)  //如果该线程是生产线程,简单设置一个拥有位
            NewValue = RTL_SRWLOCK_OWNED;
        else
        {                           //如果该线程是消费线程,设置高位为线程计数,低位为标志 拥有  和  读取
            ASSERT(FirstWaitBlock->SharedCount > 0);

            NewValue = ((LONG_PTR)FirstWaitBlock->SharedCount << RTL_SRWLOCK_BITS) |
                       RTL_SRWLOCK_SHARED | RTL_SRWLOCK_OWNED;
        }
    }

    (void)InterlockedExchangePointer(&SRWLock->Ptr, (PVOID)NewValue);  设置新的指针

    if (FirstWaitBlock->Exclusive)
    {
        (void)InterlockedOr(&FirstWaitBlock->Wake,     将第一个等待的线程唤醒,如果它是生产线程的话。
                            TRUE);
    }
    else   //如果第一个等待唤醒的是一个消费线程的等待块,根据SharedWakeChain链表, 唤醒每一个等待的消费线程。
    {
        PRTLP_SRWLOCK_SHARED_WAKE WakeChain, NextWake;
        WakeChain = FirstWaitBlock->SharedWakeChain;
        do
        {
            NextWake = WakeChain->Next;

            (void)InterlockedOr((PLONG)&WakeChain->Wake,
                                TRUE);

            WakeChain = NextWake;
        } while (WakeChain != NULL);
    }
}





  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值