锁的实现
数据结构
type Mutex struct {
state int32
sema uint32
}
- 一个mutex 总共只占8个字节,因此是一个充分使用位的数据结构
- state意义:
- 最后3位充当状态的概念
- 第0位: mutexLocked: 表示互斥锁的状态为锁定状态,既是否被某个goroutine所持有,是否已经被加锁
- 第1位: mutexWoken: 是否被唤醒(既某个goroutine尝试获取锁)
- 第2位: mutexStarving:表示当前的互斥锁处于 饥饿模式
- 剩下的则是有多少个goroutine 在等待互斥锁的释放
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5GY9i2f6-1628645052365)(/Users/joker/Nutstore Files/我的坚果云/复习/imgs/golang_mutx_数据结构.png)]
- 最后3位充当状态的概念
- sema意义:
- 充当信号量:用来唤醒goroutine
正常模式和饥饿模式
-
正常模式:
- 锁的等待者会按先进先出的顺序获取锁
- 唤醒的goroutine不会直接获取锁,而是和新请求的routine竞争获取锁,因为新请求的goroutine 占用着cpu,所以刚刚唤醒的routine大概率会失败竞争
- 锁的等待者会按先进先出的顺序获取锁
-
饥饿模式:
-
锁直接交给goroutine 等待队列中的第一个,而不会触发竞争(即时锁看上去处于unlock状态,依旧不会自旋,而是直接放入等待队列中)
-
作用:
- 防止goroutine 被饿死,既因为刚请求的goroutine更大概率获得锁,意味着之前的goroutine 很可能一直获取不到锁
-
源码分析:
hard code 定义分析
const (
mutexLocked = 1 << iota // 0001 ,代表的是,这个锁已经被加锁
mutexWoken 0010 代表的是,是否有routine被唤醒
mutexStarving 0100 代表的是,当前锁处于饥饿模式
mutexWaiterShift = iota 等待的goroutine数的位移, 如 stat>>mutexWaiterShift 就能得到当前的wait数
starvationThresholdNs = 1e6 // 1e6 ns =1ms ,既 如果当前goroutine在1ms 内获取得到锁,并且处于饥饿模式时,将锁
更正为正常模式
)
一些native 函数解释
- sync_runtime_canSpin: 自旋
- 自旋只会自旋几次,4次
- 运行于多核的机器上
- 当前机器上至少存在一个正在运行的P,并且处理的运行队列为空
- runtime.sync_runtime_doSpin
- 执行PAUSE 指令,空占CPU时间
- sync_runtime_SemacquireMutex:
- 通过golang 运行时的信号量,获取信号量
- lifo:如果为true 则会排在队头,
- runtime_Semrelease
- 通过golang的信号量,释放通知
- handoff: 如果为true,则直接按先进先出的方式,首位直接被唤醒
加锁源码分析
func (m *Mutex) Lock() {
// 先判断是否已经被加锁,并且是无等待者,是的话,直接设置为加锁即可
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m)) // 这个在test 模式下非常用于,用于查看routine之间竞争
}
return
}
m.lockSlow()
}
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state // 拷贝之前的状态
for {
// old&(mutexLocked|mutexStarving) == mutexLocked : 如果不处于饥饿模式,并且已经被其他routine所锁定
// runtime_canSpin(iter) 并且当前次数 未达到自旋次数临界值,则 自旋
// 这里会有问题: 为什么处于被其他goroutine锁定时也要进行自旋? 原因在于: 正常模式下,会试图抢占锁
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// !awoke: 表明当前goroutine 未唤醒
// old&mutexWoken == 0: 当前lock中 没有goroutine 在尝试获取锁
// old>>mutexWaiterShift 并且 没有goroutine 被阻塞
// 最后: 将自己设置为唤醒状态,并且将锁的状态也修改为唤醒
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
// 开始自旋
runtime_doSpin()
iter++
old = m.state
continue
}
// 跳出上述的for 循环表明:
// old&(mutexLocked|mutexStarving) == mutexLocked: 不满足: 既,处于饥饿模式下,或者是 锁已经被人释放了
// runtime_canSpin(iter): 达到自旋次数最大值
new := old
// 如果处于饥饿模式下, 不抢占,直接表明为locked (然后把自己丢到排队去)
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 处于饥饿模式,或者是被抢占了,则直接排队处理
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 如果在饥饿模式下被抢占了,则更新锁为饥饿锁,也就是说,如果不是饥饿模式或者锁没有被抢占则不会设置为饥饿模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 如果该goroutine被唤醒,要么是获得了锁,要么是处于休眠状态,总之不能是woken状态
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// 清除状态
new &^= mutexWoken
}
// cas 交换更新锁状态: 可能只是更新为饥饿模式,不一定代表就是获取得到了锁
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果锁之前是未锁定,且不处于饥饿状态,则表明,我们获得了锁,直接退出
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// 如果该goroutine为新的goroutine,则queueLifo 为false
// 否则为唤醒的goroutine,为 true
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 该函数的作用为: 如果是新来的goroutine,放到队尾,否则是唤醒的goroutine,放到队头
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 判断该goroutine 是否处于饥饿模式 (通过上述的starving 以及时间间隔)
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 如果该锁处于饥饿模式
if old&mutexStarving != 0 {
// 那么根据golang锁的定义,如果是饥饿模式下的锁,获取锁的必须是对头(既本goroutine),
// 也就是说,当该goroutine被唤醒之后,锁不可以被别人给抢占,也不会有其他goroutine被唤醒去抢占
// 并且因为是饥饿模式,所以当前goroutine肯定是在等待队列中的,因此 old>>mutexWaiterShift 不能为0
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 锁等待-1
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 如果当前goroutine是队列中最后一个,则退出饥饿模式
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
// 更新state,因为在上面我们已经 - 去了一些状态(如饥饿模式,等待数等),所以直 add即可
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
解锁源码分析
- 注意点
- lock 可以被其他goroutine unlock
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 先尝试直接解锁,如果为0,表明解锁成功
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Outlined slow path to allow inlining the fast path.
// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
// 解锁的前提是加锁,如果加锁没有,则是一个错误的状态,直接抛异常
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// 如果处于饥饿模式下
if new&mutexStarving == 0 {
old := new
for {
// 如果没有goroutine在抢占锁了,或者是
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 调用 sema.go 唤醒一个goroutine
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// 在饥饿模式下,handoff为true,直接唤醒的是第一个goroutine
// 参数handoff true 代表着直接唤醒等待队列goroutine中的第一个, 被唤醒的第一个,基于饥饿模式下,会直接获得锁
// 而新来的goroutine会进到队尾
runtime_Semrelease(&m.sema, true, 1)
}
}
读写锁的实现
- What:
- 并发读之间不互斥
- 并发写之间互斥
- 并发读和写互斥
数据结构
type RWMutex struct {
w Mutex // 既写锁,用于复用
writerSem uint32 // 写等待读的数
readerSem uint32 // 读等待写的数
readerCount int32 // 当前正在读的goroutine数
readerWait int32 // 当写操作时,读等待的数
}
加读锁
func (rw *RWMutex) RLock() {
// debug 模式下启用有很大好处
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// readerCount+1 ,如果readerCount<0 则认为是被写锁占用着,此时goroutine 等待阻塞被唤醒
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
解读锁
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
// 如果 >=0 ,说明解锁成功
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// Outlined slow-path to allow the fast-path to be inlined
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")
}
// 如果有写操作在等待,则释放信号量,让写操作尝试竞争获取锁
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
加写锁
func (rw *RWMutex) Lock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 可能有多个routine在加锁,则复用之前的锁机制代码
rw.w.Lock()
// Announce to readers there is a pending writer.
// 将readerCount 剪到最小(所以在加读锁或者解读锁的时候可以通过这个值来判断是否有写routine)
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// 将读操作等待的routine数追加到 写操作等待读的数 ,不为0 ,所有当前有读锁,则等待信号量的获取
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}
解写锁
func (rw *RWMutex) Unlock() {
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}
// 先还原当加锁之前有多少个读routine在等待读取
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
throw("sync: Unlock of unlocked RWMutex")
}
// 因为写锁被占用之后,所有的读routine都被阻塞,所以要挨个唤醒
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// 可能此时还有其他的写锁routine 在尝试获取锁,则 转交给mutex操作
rw.w.Unlock()
if race.Enabled {
race.Enable()
}
}
总结
-
读写锁
- 读锁:
- 加读锁的时候,会先判断写操作是否已经占用,是的话,则会等待信号量获取
- 解读锁的时候,同样的也是基于readCount是否<0 ,小于0则会去尝试唤醒写routine
- 写锁:
- 加写锁的时候,会先交给写锁routine之间竞争获取,然后再判断是否有读routine,有的话则阻塞,否则的话,设置状态表明有写routine了
- 解写锁的时候,会先唤醒所有因为该写锁而阻塞的读routine,然后再交给mutex 释放写锁(因为期间可能还有其他写锁在)
- 读锁:
-
golang的锁有2种模式
- 正常模式和饥饿模式
- 正常模式,gorputine之间会竞争获取锁,饥饿模式下,排队的形式获取锁
- 当饥饿模式下,如果刚好最后一个goroutine获得锁,或者是获取锁的时间间隔在1ms内,则会恢复为正常模式
- golang的锁充分运用位运算,低3位代表了 是否被锁定,是否有routine唤醒,以及锁的状态,剩下的位数则是routine的等待数
- goroutine的唤醒与阻塞都是基于golang 的信号量机制实现
问题
-
读写锁:
- 写锁饥饿怎么处理,既 n个读routine,1个写routine
- A: golang的读解锁的时候,会尝试唤醒写锁的routine,如果有写等待的话
- 为什么读加锁,如果readerCount<0 就被阻塞
- A: 因为 golang 的读写锁加了写锁之后,会将readCount 会赋值一个 增量的负值来表明 有写routine了,而读是需要等待写的(当然写也要等待读)
- 为什么读解锁,如果readerCount>=0 就认为解锁成功
- a. golang的读写锁是可以被其他routine给解开的
- b. readerCount<0的情况为,只有当写锁抢占的时候会<0 ,所以是要阻塞等待的
- 如何防止 读解锁之前是没有被RLock的
- 是通过 内部的readerCount 判断的,readerCount 是当前正在读的goroutine数,没RLock表明为0 或者是被写锁 -maxRead ,
- 写锁饥饿怎么处理,既 n个读routine,1个写routine
-
什么是PAUSE指令
- 使得进程(线程)挂起,直到收到信号,且信号函数返回成功,pause函数才会返回
-
解锁问题
-
为什么直接-mutexLocked 然后判断结果==0 就可以判断是否解锁成功,不为0的情况有哪些
-
为什么饥饿模式下,唤醒的时候,不需要判断是否
-
-
从正常模式到饥饿模式的触发条件
- starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
- **既当自旋时间超过了starvationThresholdNs 就会进入饥饿模式, starvationThresholdNs 为1e6 纳秒,既1ms **
-
从饥饿模式恢复到正常模式的条件
-
如果该goroutine获取得到了锁,并且满足下面任意一个条件都会转为正常竞争模式
- 该goroutine为队列中最后一个
- 等待的时间小于1ms