Loading

Golang sync.RWMutex 解析

表现

读写锁在表现上是允许并发读,独占写的。这把锁理解上可以看成一把读锁(共享锁),一把写锁(独占锁)

即调用读写锁时,一读线程持有读锁(RLock()),同时允许其它线程持有读锁,大家一起进行并发读。但是写线程(Lock())持有的写锁是独占锁的,当别人持有读或写锁,它就无法请求获得写锁。

总结:

writer 线程持有读写锁,这把锁就是写锁-独占锁
reader 线程持有读写锁,这把锁就是读锁-共享锁

RWMutex 结构体

type RWMutex struct {
	w           Mutex  // 互斥锁
	writerSem   uint32 // reader 完成后会释放 writer信号量
	readerSem   uint32 // writer 完成后会释放 reader 信号量
	readerCount int32  // 当前的 reader 的数量(以及用来判断是否有 writer 线程请求锁或者持有锁)
	readerWait  int32  // writer 等待 reader 完成的数量,到 0,writer 会被唤醒
}

const rwmutexMaxReaders = 1 << 30 // 表识最大 reader 线程数量

RLock()

func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 当 reader 线程请求读锁,发现 readercout + 1 < 0
	// 1. 为readerCount += 1
	// 2. 判断 readerCount 是否小于 0
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		// 证明有一个 writer 线程正在等待,阻塞自身,等待writer 线程完成
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

其中值得注意的是:

  • readerCount 为负数,表示当前时刻有 writer 线程正在等待写锁 或者 持有写锁,因为写锁是独占锁,这种情况下,reader 线程应当进入阻塞等待 writer 线程完成释放 readerSem 信号量
  • readerCount 为正数,表示此时 reader 线程的数量

RUnLock()

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
    // 将 readerCount 数量 - 1
    // 判断此时 readerCount 数量是否小于 0
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		// 如果此时 readerCount 数量小于 0,说明有 writer 线程等待锁
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}

func (rw *RWMutex) rUnlockSlow(r int32) {
    
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		throw("sync: RUnlock of unlocked RWMutex")
	}
    // 如果此时 readerWait 数量,写线程[需要等待]的 reader 线程已经全部完成(不是 readerCount) -- readerWait == 0
	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
		// 则释放 writerSem 信号量唤醒 writer 线程
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

值得注意的是:

readerWait 只有当 writer 线程请求锁时才会出现为非负数,此时它会拷贝readerCount 的大小,即 readerWait = readerCount, 如果咩有 writer 线程,则 readerWait 的值为 0, readerCount 的大小为正数

总结:

当 writer 线程请求锁时,比它后来的 reader 线程不会影响到 readerWait 的数量,即 后面的 reader 线程是无法与 writer 线程竞争锁的,讲究先来后到,避免写锁饥饿。

Lock()

值得注意的是,因为写锁是一把独占锁,所以所有的 wrtier 线程是竞争的关系,利用 RWMutex 的结构体成员 sync.Mutex 互斥锁保证

func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
    // 首先与其他 writer 线程竞争
	rw.w.Lock()
    // 通过将 readerCount 的数量变为负数,告诉 reader 线程这里有 writer 线程正在等待锁
    // 注意这里的 r 是局部变量
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// Wait for active readers.
    // 判断 r ,不为 0 说明有 reader 线程持有锁
    // 且将此时的 r 赋值给 readerWait 成员,判断 readerWait 的数量
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        // 请求写信号量,阻塞自身,等待 reader 线程释放 writer 信号量
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

值得注意的是:

当 writer 线程请求锁后,此时真正活跃的 reader 线程只有 readerWait 的数量,后面的 reader 线程都因为 readerCount 为负数(有 writer 线程请求锁或者持有锁)陷入阻塞状态。

直到 active reader 线程都执行完毕,则唤醒 writer 线程。

UnLock

func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}
	// 将 readerCount 数量变为正数,告诉 reader 线程,无 writer 线程了
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		race.Enable()
		throw("sync: Unlock of unlocked RWMutex")
	}
    // 将此时所有的 reader 线程唤醒
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// 释放互斥锁,唤醒 writer 线程竞争互斥锁
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

参考资料

  1. 《Go 专家编程》作者:任洪彩 - 第五章 并发控制 - 书籍
  2. 《Go 并发编程实战课》作者:晁岳攀 - 基本并发原语 - 极客时间
posted @ 2021-09-03 09:50  zhixlin  阅读(504)  评论(0编辑  收藏  举报