一. 什么是读写锁
很多时候,对共享变量的访问有以下特点:大多数情况下线程只是读取共享变量的值,并不修改,只有极少数情况下,
线程才会真正地修改共享变量的值。对于这种情况,读请求之间之间是无需同步的,他们之间的并发访问是安全的。但是
必须互斥写请求和其他读请求。
这种情况在实际中是存在的,比如配置项。大多数时间内,配置是不会发生变化的,偶尔会出现修改配置的情况。如果
使用互斥量,完全阻止读请求并发,则会造成性能的损失。处于这种考虑,POSIX引入了读写锁。
二. 读写锁API
1. 读写锁属性
读写锁属性(pthread_rwlockattr_t)有两种: lockkind和pshared。
(1)lockkind:读写策略,包括读取优先(默认属性)、写入优先。
读取优先:如果在写锁请求后面到来的读锁请求不被写锁请求阻塞。如果读锁请求前仆后继源源不断地到来,只要有
一个读锁没完成,写锁就没分。 该策略会导致较早到的写锁饿死。
写入优先:一旦线程申请写锁,在写锁请求后面到来的读锁请求就会统统被阻塞,不能先于写请求拿到锁。
enum { PTHREAD_RWLOCK_PREFER_READER_NP, //读者优先(默认属性) PTHREAD_RWLOCK_PREFER_WRITER_NP, //读者优先 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, //写者优先 PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP //读者优先 };
/* 获取与设置属性 */
int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t * attr, int * pref);
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t * attr, int * pref);
(2)pshared
PTHREAD_PROCESS_PRIVATE: 进程内 竞争读写锁 -- 默认属性
PTHREAD_PROCESS_SHARED: 进程间 竞争读写锁
// 设置pshared属性 int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); // 获取pshared属性 int pthread_rwlockattr_getpshared(pthread_rwlockattr_t *attr, int *pshared);
1. 创建
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
2. 获取
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr) //读锁 int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr) //写锁
3. 释放
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr) //将读锁或写锁解锁
4. 销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
三. 读写锁实现原理
变量 | 说明 |
__lock | 管理读写锁全局竞争的锁,无论是读锁写锁还是解锁,都会互斥 |
__writer | 写锁持有者的线程ID,如果为0则表示当前无线程持有写锁 |
__nr_reads | 读锁持有线程的个数 |
__nr_reads_queued | 读锁的派对等待线程的个数 |
__nr_writers_queued | 写锁的排队等待线程的个数 |
无论是申请读锁还是申请写锁,还是解锁,都至少会做一次全局互斥锁(对应__lock)的加锁和解锁,若不考虑阻塞,单单考虑
操作本身的开销,读写锁的加解锁开销是互斥锁的两倍。当然,函数结束前或进入阻塞之前,会将全局的互斥锁释放。
1. 对于读锁请求而言,如果:
(I) 无线程持有写锁,即__writer==0
(II) 采用的是读者优先策略或没有写锁的等待者(__nr_writers_queued==0)
当满足这两个条件时,读锁请求都可以立即获得读锁,返回之前执行__nr_readers++,表示增加了一个线程占有读锁。
不满足的话,则执行__nr_readers_queued++,表示增加一个读锁等待者,然后调用futex,陷入阻塞。醒来之后,会执行
__nr_readers_queued–,然后再次判断是否同时满足条件1和2
2. 对于写锁请求而言,如果:
(I) 无线程持有写锁,即__writer==0
(II) 没有线程持有读锁,即__nr_readers==0
只要满足上述条件,就会立刻拿到写锁,将__writer置为线程的ID(调度域)
如果不满足,那么执行__nr_writers_queued++,表示增加一个写锁等待者线程,然后执行mutex陷入等待。醒来后,先执行
__nr_writers_queued–,然后重新判断条件1和2。
3. 对于解锁而言,如果当前锁是写锁,则执行如下操作:
执行__writer=0,表示释放写锁
根据__nr_writers_queued判断有没有写锁等待者,如果有则唤醒一个写锁等待者
如果没有写锁等待者,则判断有没有读锁等待者;如果有,则将所有的读锁等待者一起唤醒。
如果当前锁是读锁,则执行如下操作:
执行__nr_readers–,表示读锁占有者少了一个
判断__nr_readers是否等于0,是的话则表示自己是最后一个读锁占有者,需要唤醒写锁等待者或者读锁等待者:
根据__nr_writers_queued判断是否存在写锁等待者,若有,则唤醒一个写锁等待线程
如果没有写锁等待者,判断是否存在读锁等待者,若有,则唤醒所有的读锁等待者