读写锁,Slim Reader-Writer Locks(以下简称SRWLock),也是用户线程同步问题的一种方法。
典型的线程同步问题中有一类叫“读者-写者问题”。当一个写者正在对某些资源进行“写”的时候,任何其他读者以及任何其他写者都无法获得这些资源。当有一个读者正在对某些资源进行“读”的时候,允许其他读者进入一起“读”,但是绝不运行有任何写者进来“写”这些资源。这里,“写”的含义就是“修改”,“读”的含义就是“只使用不修改”。
换句话说,就是“多个读者可以共享资源,单个写者独占资源”。
该机制规定,任何一个读者或写者在访问资源之前都要对该资源加锁,加锁成功后才可以访问该资源;同时,当不再需要访问资源的时候要释放所加的锁。
因此,便有了“读写锁”机制。还有一种叫法,叫“排他锁”和“共享锁”。该机制是怎么规定的:
1、以读者为主体:一个“读者”想要访问资源时,首先给资源加上“读锁”(共享锁),如果加锁成功,方可访问该资源。
2、以写者为主体:一个“写者”想要访问资源的时候,必须先给资源加上”写锁“(排他锁),加锁成功,才可以访问这个资源。
3、以资源作为体:如果一个写锁(排他锁)被加在这个资源上,那么任何其他的写锁和读锁(共享锁)都无法继续加在这个资源上了,这样就保证了写者在修改该资源的时候不被资源破坏;当一个读锁被加在这个资源上之后,只能允许读锁继续对该资源加锁,而不允许写锁加在这个资源上,这样就保证了当“至少”有一个读者在使用该资源的时候,资源不会被写者修改而破坏。
我们把这里的“读者”和“写者”看做两种类型的线程,这样,就使用读写锁的方法来处理线程同步问题。需要注意的是(在Windows Vista以前的系统中,没有与读写锁机制有关的API,所以本文中的API不适用用于Vista版本以前的Windows系统)。
首先,要定义分配一个SRWLOCK结构,然后初始化它,调用函数:InitializeSRWLock,传递一个该结构的指针。
该函数会在内部设置SRWLOCK结构的内部数据,该结构内部的成员对你而言应该是透明的,你不应该自己去设置。
一旦一个SRWLock被初始化之后,一个“写者线程”就可以请求一个“排他锁”来访问被保护的资源。调用函数:AcquireSRWLockExclusize,传递一个已经初始化了的SRWLOCK结构的地址该它。
当“写者线程”完成了对资源的修改,可以通过ReleaseSRWLockExclusive函数来释放开始请求访问的排他锁。传递一个在请求时候使用的SRWLOCK结构的地址。VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
相对地,对于“读者线程”,可以使用以下两个函数来请求访问(给资源加共享锁)和释放共享锁。
与关键代码段不同的是,对于读写锁而言,在一个线程内,没有必要为一个资源多次请求访问权。因为不能多次加上同一个排他锁,而共享锁一旦加上就没有必要再加了。所以,不需要多次请求一个资源锁,也不需要多次释放同一个锁。
另外,使用读写锁,只有初始化读写锁的函数InitializeSWRLock,却没有对应删除或销毁读写锁的函数,至于销毁或删除读写锁,这个是由系统自动维护的。
本书该节还对各个用户模式下的线程同步机制做了一个性能上的比较,感觉没有必要详述,只要知道结果就可以了。
从运行速度上来看:互锁函数 > 读写锁 ≥ 关键代码段 >> 互斥内核对象
上面凡是出现“=”的情况,表示在单个线程中的结果。
可见,用户模式的线程同步效率是高于内核模式的(互斥内核对象运行在内核模式)。而读写锁的的效率要高于关键代码段,因为当争用的时候,关键代码段会在内部创建一个事件内核对象(运行于内核模式),而且读写锁中的读锁实现了资源共享,多线程下效率比资源独占的关键代码段要高。
因此,推荐使用读写锁代替关键代码段。
最好要注意的就是,Vista以前的Windows系统不支持读写锁的API,因此这些函数无法在Vista以前的Windows系统中使用。在这些系统中,还是推荐使用关键代码段吧。