1 读写锁简介
互斥量要么是加锁状态,要么是不加锁状态,而且一次只有一个线程对其进行加锁。读写锁可以有3种状态:读加锁状态、写加锁状态和不加锁状态。一次只有一个线程可以占有写模式读写锁,但是可以有多个线程同时占有读模式的读写锁。因此读写锁比互斥量具有更高的并行性。
当读写锁处于写加锁状态时,所有试图对这个锁加锁的线程都将被阻塞。当读写锁处于读加锁状态时,所有试图以读模式对这个锁访问的线程都将得到访问权,而以写模式对这个锁访问的线程都将被阻塞,需要等到所有读线程释放锁写模式线程才能得到访问权。当然了,如果读写锁当前处于读模式,有线程请求写模式,那么接下来的读模式请求都将被阻塞,这样可以避免读模式锁长期被占用,而写模式请求得不到满足。
读写锁在使用之前必须初始化,在释放底层内存之前必须被销毁。
2 读写锁API函数简介
下面对Single UNIX Specification的读写锁的API进行简介。
2.1创建读写锁
读写锁是pthread_rwlock_t类型的。读写锁既可以像静态变量那样分配,也可以在运行时动态创建(比如通过malloc()在一块内存中分配)。对于静态初始化把常量PTHREAD_ RWLOCK_INITIALIZER赋值给读写锁。
pthread_ rwlock _t mtx = PTHREAD_ RWLOCK _INITIALIZER;
对于动态创建读写锁可以调用函数pthread_rwlock_init()来完成。函数格式如下:
#include<pthread.h>
int pthread_ rwlock_init( pthread_ rwlock_t *restrict rwlock,
const pthread _ rwlockattr_t *restrict attr);
参数说明:
rwlock —— 创建的读写锁
attr —— 读写锁属性,NULL代表读写锁的各种属性取默认值。返回值:初始化成功返回0;否则返回错误编号。
2.2 销毁读写锁
当不需要自动或者动态分配的读写锁时,必须使用pthread_ rwlock_destroy()函数将其销毁。函数格式如下:
#include<pthread.h>
int pthread_ rwlock_destroy( pthread_ rwlock_t * rwlock );
参数说明:
rwlock —— 需要销毁的的读写锁
返回值:销毁成功返回0;否则返回错误编号。
2.3 加锁
采用函数pthread_rwlock_rdlock()来对读写锁进行读模式加锁。采用函数pthread_rwlock_wrlock()来对读写锁进行写模式加锁。函数格式如下:
#include<pthread.h>
int pthread_rwlock_rdlock ( pthread_ rwlock_t * rwlock);
int pthread_rwlock_wrlock ( pthread_ rwlock_t * rwlock);
参数说明:
rwlock—— 需要加锁的的读写锁
返回值:加锁成功返回0;否则返回错误编号。
如果线程不希望被阻塞,可以调用pthread_ rwlock_tryrdlock()和pthread_ rwlock_trywrlock()来尝试加锁。如果不可以获取锁时,这两个函数都返回EBUSY。函数具体格式如下:
#include<pthread.h>
int pthread_ rwlock_tryrdlock( pthread_ rwlock_t * rwlock);
int pthread_ rwlock_trywrlock( pthread_ rwlock_t * rwlock);
参数说明:
rwlock—— 需要尝试加锁的读写锁
返回值:加锁成功返回0;否则返回EBUSY。
与互斥量一样,Single NUIX Specification 提供了带有超时的读写锁加锁函数,使应用程序在获取读写锁时避免陷入永久阻塞状态。当线程试图访问一个已经加锁的读写锁时,函数pthread_ rwlock_timedrdlock()和pthread_ rwlock_timedwrlock()允许设定线程阻塞时间。这两个函数与pthread_ rwlock_rdlock()和pthread_ rwlock_wrlock()函数基本上是等价的,但是在达到超时时间值时,pthread_ rwlock_timedrdlock()和pthread_ rwlock_timedwrlock()不会对读写锁进行加锁,而是返回错误码ETIMEOUT。函数原型如下:
#include<pthread.h>
int pthread_ rwlock_ timedrdlock( pthread_ rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr );
int pthread_ rwlock_ timedwrlock( pthread_ rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr );
参数说明:
rwlock—— 需要加锁的读写锁
tsptr —— 超时时间,线程愿意等待的绝对时间(与相对时间对比而言,指定在时间X之前可以等待,而不是愿意阻塞Y秒),用timespec结构来表示的(用秒和纳秒来描述时间)。
返回值:加锁成功返回0;否则返回EBUSY。
2.4 解锁
如果线程对读写锁访问结束,需要调用函数pthread_ rwlock_unlock()对读写锁进行解锁。下列行为均属于错误应该避免:对未加锁的信号量解锁,解锁其他线程锁定的读写锁。函数格式如下:
#include<pthread.h>
int pthread_ rwlock_unlock( pthread_ rwlock_t * rwlock);
参数说明:
rwlock—— 需要解锁的读写锁
返回值:解锁成功返回0;否则返回错误编号。
3 读写锁适用场合讨论
读写锁特别适用于对数据结构读的次数远远大于写的次数的场合,但是也要慎用读写锁。一个非常容易犯的初级错误就是当看到一个数据读的次数远远大于写的次数就用读写锁。看此很合理但是有一个前提条件,那就是加锁的时间持续很长。如果每次加锁的时间很短还用读写锁,那么程序运行效率还不如使用互斥量。
为什么会出现上述的情况呢。因为读写锁更新自己状态时必须是原子操作,在目前缺少精妙而且节省资源的同步方式之下,读写锁用一个字节来存储读的次数。因为读的次数必须原子更新,因此获取读锁与获取互斥量有着相同的资源需求,也就是有着相同的资源开销。
也就是说使用读写锁时,如果与互斥量获取锁的次数一样的话,资源开销上是一样的,而且还可能比互斥量稍微多一点。那为什么还要用读写锁呢?在一些特殊情况下,获取锁的时间比较长,这个时候用读写锁就可以让多个线程并发的去读,从而提高效率。但是这些特殊的任务还需要满足另外一个特点——读的次数远多于写。如果读写次数差不多,一次读一次写,那和用互斥量几乎一样。如果锁持有的时间很短,读的时候只读取几十个字节的内存,还用读写锁,那么可能会造成系统性能的下降,既然读取已经很快,还有必要并发的读吗?
综上,在使用读写锁时,一定要仔细分析应用场景。
【参考】
1、《Unix环境高级编程》 作者:(美)理查德•史蒂文斯 (美)拉戈 译者:张亚英 戚正伟
2、《慎用读写锁》 作者:ysu108 http://blog.csdn.net/ysu108/article/details/39343295