本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。
Slim读/写锁
SRWLock的目的和关键段相同,对一个资源进行保护,构造了一段“原子访问”的代码,不让其他线程访问它。但与关键段不同的是SRWLock允许区分想要读取资源值的线程和想要写入资源值的线程,因为仅仅读取资源是不会破坏数据的,下面是Slim读/写锁的简单用法:
SRWLOCK g_srwLock ... //init SRWLock InitializeSRWLock(&g_srwLock); ... //当需要写入资源的时候申请"排他锁" AcquireSRWLOckExclusive(&g_srwLock); //执行写入动作 ... //写入结束后释放"排他锁" ReleaseSRWLockExclusive(&g_srwLock); //----------------------------------------- //当需要读取时申请"共享锁" AcquireSRWLockShared(&g_srwLock); //执行读操作 ... //读取结束后释放"共享锁" ReleaseSRWLockShared(&g_srwLock); //系统会自动清理g_srwLock,没有别的清理函数
可以看到,存在两种锁用于此机制:"排他锁"和"共享锁"。排他锁需要任何中锁都不存的情况下才能返回,否则阻塞;共享锁在没有排他锁的情况下就可以返回,哪怕有其他共享锁也没关系。这样在写操作之前申请排他锁,在读之前申请共享锁就可以保证共享资源数据不会被意外破坏。另外,只有初始化函数而没有释放函数。
看似SRWLock更关键段十分相似,但相比关键段SRWLock缺乏下面两个特性:
1、不存在对应的TryEnterXXX函数,如果锁已被占用那么只能选择阻塞调用线程;
2、不能递归的获得SRWLock。也就是说一个线程不能为了多次写入资源而多次锁定资源。而关键段可以做到。回想一下,因为关键段在Enter的时候将判断当前线程是否是共享资源的占有者,如果是则会“放行”,并增加引用计数。然而SRWLock始终不关心调用线程是谁。
SRWLock配合条件变量
SRWLock的另一个有用之处就是能配合“条件变量”使用,来完成一些更复杂的同步任务。我们假设如下场景:有一个共享的队列,2个服务端线程负责读取队列中的条目以处理,2个客户端线程负责写入队列中的条目以使服务先端线程处理,当队列中没有条目的时候应当挂起服务端线程,直到有条目进入时才被唤醒,另一方面,当队列已满时,客户端线程应当挂起直到服务端至少处理了一个条目,以释放至少一个条目的空间。
首先创建几个共享资源锁,其中SRWLOCK是上文提到的读写锁,CONDITION_VARIABLE就是这里的条件变量:
SRWLOCK g_srwLock; CONDITION_VARIABLE g_cvReadyToProduce;//读取线程用于通知写入线程可以开始写入 CONDITION_VARIABLE g_cvReadyToConsume;//写入线程用于通知读取线程可以开始读取
设计如下客户端写入线程流程:
AcquireSRWLockExclusive(&g_srwLock):在写入之前获得排他锁,如果有其他锁,则阻塞;
SleepConditionVariableSRW(&g_cvReadToProduce,&g_srwLock,INFINITE,0):当队列已满时,等待g_cvReadToProduce变量信号(此信号应该由做读取操作的服务端线程发起)。参数:&g_srwLock,INFINITE,0 分别表示暂时释放g_srwLock锁,并永久等待变量信号;
Write Queue...:对队列的写操作,如果在上一步时经过Sleep,并临时释放了g_srwLock锁,在这一步会自动重新获得g_srwLock锁;
ReleaseSRWLockExclusive(&g_srwLock):写完队列后释放排他锁;
WakeAllConditionVariable(&g_cvReadToConsume):向所有正在等待队列中条目的服务端线程发起g_cvReadToConsume信号,通知他们开始读取队列;
设计如下的服务端读取流程:
AcquireSRWLockShared(&g_srwLock):在写入之前获得共享锁,如果有排他锁,则会阻塞;
SleepConditionVariableSRW(&g_cvReadToConsume,&g_srwLock,INFINITE,CONDITION_VARIABLE_LOCKMODE_SHARED):当队列空时,等待g_cvReadToConsume变量信号(此信号应该由做写入操作的客户端线程发起)。参数:&g_srwLock,INFINITE,CONDITION_VARIABLE_LOCKMODE_SHARED 分别表示共享g_srwLock锁(不释放),并永久等待变量信号;
Read Queue...:对队列的读操作;
ReleaseSRWLockShared(&g_srwLock):读完队列后释放共享锁;
WakeAllConditionVariable(&g_cvReadToProduce):向所有正在等待队列至少有一个空间条目的客户端线程发起g_cvReadToProduce信号,通知他们可以开始写入队列;
上面的过程中我们对一个场景做了设计用到了SRWLock和所谓的条件变量,总结一下条件变量的用法如下:
CONDITION_VARIABLE g_cvReadyToProduce; //创建变量 VOID WINAPI InitializeConditionVariable( __out PCONDITION_VARIABLE ConditionVariable ); //等待变量信号 BOOL WINAPI SleepConditionVariableSRW( __inout PCONDITION_VARIABLE ConditionVariable, __inout PSRWLOCK SRWLock, __in DWORD dwMilliseconds, __in ULONG Flags ); //发出变量信号,以唤醒正在等待变量的线程 VOID WINAPI WakeAllConditionVariable( __inout PCONDITION_VARIABLE ConditionVariable );
前面一篇讲到关键段的时候也提到过条件变量,事实上,关键段也可以使用条件变量,只是API不太相同:
BOOL WINAPI SleepConditionVariableCS( __inout PCONDITION_VARIABLE ConditionVariable, __inout PCRITICAL_SECTION CriticalSection, __in DWORD dwMilliseconds ); VOID WINAPI WakeAllConditionVariable( __inout PCONDITION_VARIABLE ConditionVariable );
另外,除了WakeAllConditionVariable还有一个WakeConditionVariable,顾名思义,后者是单单唤醒一个正在等待变量的线程,这类似于”事件的自动重置”。
关于更多条件变量的信息可以参见:Using Condition Variables
劳动果实,转载请注明出处:http://www.cnblogs.com/P_Chou/archive/2012/06/24/slim-rw-read-in-thread-sync.html