读写锁的定义
- 读操作与写操作互斥;
- 写操作与写操作互斥;
- 读操作可以并发;
主要的API:
Golang读写锁的实现
golang读写锁的结构体如下:
go version go1.16.4 darwin/amd64
const rwmutexMaxReaders = 1 << 30
无竞争状态下加解锁
Lock && Unlock
func (rw *RWMutex) Lock() {
rw.w.Lock()
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
+ rwmutexMaxReaders
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
执行完Lock之后,状态变化为:
那unlock也比较好理解了,还原就可以了;
func (rw *RWMutex) Unlock() {
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
rw.w.Unlock()
}
Rlock && Runlock
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
执行了一次或者多次rlock后:
再看下runlock:
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
}
可以看到runlock就是将readerCount恢复原状;
无竞争状态下读写锁的简单总结如下:
竞争状态下的加解锁
Lock状态下执行Lock与Rlock
func (rw *RWMutex) Lock() {
rw.w.Lock()
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
+ rwmutexMaxReaders
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
在lock下,可以看到
- 所有后续的lock全部阻塞在 rw.w.lock()上;
- 通过状态:rc小于0 标示锁被写占用, 使得后续的读阻塞在信号量readerSem上;
- 同时rc 为 -rwmutexMaxReaders+n, n为被阻塞的读操作的数量;
执行unlock时:
func (rw *RWMutex) Unlock() {
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
rw.w.Unlock()
}
总结下:在写锁先占用的情况下:
- 写锁释放时,
- 所以:读写锁的状态变化为:写占用-读占用
- 在写锁期间,被阻塞的读操作一定先执行:写->读
- 读锁并不知道写操作阻塞的情况,见缝插针的执行;
接下来我们再看下这m个被阻塞的写操作如何翻身,又用到了哪些属性,以及读写锁:写-读 之后的下一个状态是什么,一定是读?一定是写,还是读写都有可能?
Rlock状态下执行Lock与Rlock
已经rlock了n次;
func (rw *RWMutex) Lock() {
rw.w.Lock()
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
+ rwmutexMaxReaders
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
此时,执行runlock会怎么样:
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
}
此时,又变为lock下的lock与rlock了;
总结下:在读锁先占用的情况下
- 写操作通过状态:rc < 0 标识有写等待;这样后续的读被阻塞;
- 读释放时:发现写等待,就会唤醒写;
- 因此:读占用时,读写锁的状态转化为:读占用-写占用;
golang 读写锁的总结:
- 在读占用的情况下,写操作的到来将读操作划分成了 已经占用锁的读操作和未占用读锁的读操作两部分;待已占用的读释放完全,再唤醒写;
- 在写占用的情况下,写操作释放锁时,唤醒写锁期间到来的读操作,然后再释放写锁;
- 将上面的状态转化连起来,推论:在读写高并发时,golang的实现就变为:写(1),读(n),写(1),读(j)……
- 有一个遗留的问题:m+z的lock操作都阻塞在 rw.w.lock() 上,如何确定下一个w写锁由谁获取?
- 按照写操作来的顺序?
- 最新的写操作?
- Both?
- 即: java读写锁的公平模式与非公平模式
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
C++读写锁的实现简单分析
stl的shared_mutex
class shared_mutex {
std::mutex mut_;
std::condition_variable gate1_;
std::condition_variable gate2_;
unsigned state_;
/* example:
EXCLUSIVE_WAITING_BLOCKED_MASK == 0x80000000;
MAX_SHARED_COUNT_MASK == 0x7fffffff;
NO_EXCLUSIVE_NO_SHARED == 0x00000000;
*/
public:
shared_mutex() : state_(NO_EXCLUSIVE_NO_SHARED) {}
// Exclusive ownership
void lock();
void unlock();
// Shared ownership
void lock_shared();
void unlock_shared();
};
runlock(): 如果gate2有阻塞,则触发gate2;
unlock(): gate1.notifyall()
boost的shared_mutex
class shared_mutex {
struct state_data {
unsigned shared_count; // 读者数量
bool exclusived; // 表示加了写锁
bool exclusive_entered; //表示即将加写锁、
// bool upgrade // etc
};
state_data m_state;
boost::mutex m_mutex_state; // 对应golang的原子操作?
boost::condition_variable m_shared_cond;// readSem?
unlock时 或者unlock_shared()时:
m_exclusive_cond.notify_one()
m_shared_cond.notify_all()
Ps: boost提供读锁升级到写锁;
Java读写锁的实现的简单分析
public KReentrantReadWriteLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this); }
}
非公平锁:
readshouldblock: head.next是否是写锁
writeshouldblock: false
公平锁:
readshouldblock: hasQue
writeshouldblock: hasQue
一些额外的特性:
- 锁的可重入性;
- 锁降级:获取写锁之后,可以降为读锁;
- 锁的公平性与非公平性:非公平锁与公平锁;
读写锁实现总结
参考:
读写锁ReentrantReadWriteLock源码分析_ThinkWon的博客-CSDN博客
C++并发型模式#7: 读写锁 - shared_mutex | 邓作恒的博客
读写锁ReadWriteLock的实现原理