本系列意在记录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;//写入线程用于通知读取线程可以开始读取

设计如下客户端写入线程流程:

image

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信号,通知他们开始读取队列;

 

设计如下的服务端读取流程:

image

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