目录

接上面的锁来聊一聊go的mutex

Golang mutex

mutex的由来Mutual exclusion(互斥)的前缀组合,俗称互斥体/互斥锁

mutex的数据结构

https://cdn.cjpa.top/image-20210406134531905.png

因为足够简单,因此不需要额外初始化,这个结构体的零值就是一个有效的互斥锁,处于unlocked状态。

https://cdn.cjpa.top/image-20210406134657389.png

state存储互斥锁的状态,加锁和解锁都是通过atomic包提供的函数原子性操作该字段

sema用作信号量,主要用做等待队列。

mutex的两种模式

正常模式

在正常情况下,尝试加锁的goroutine会先自旋,通过原子操作获得锁。

如果几次自旋之后仍然获取不到锁,则通过信号量等待

https://cdn.cjpa.top/image-20210406140621310.png

所有的等待者都会按照先入先出的顺序排队等待

当锁被释放,第一个等待者并不会直接拥有锁,而是会和那些处于自旋状态,尚未排队等待的goroutine,这种情况下,正在自旋的goroutine更有优势,原因如下:

  • 处于自旋状态的goroutine本身就正在cpu上面运行
  • 自旋的goroutine有很多个而被唤醒的只有一个,因此被唤醒的goroutine很大概率上拿不到锁

https://cdn.cjpa.top/image-20210406141349526.png

这种情况下这个goroutine会被重新插入到队列的头部,而不是尾部。

https://cdn.cjpa.top/image-20210406141545062.png

饥饿模式

当一个goroutine本次加锁等待时间超过了1ms之后,它会把当前的Mutex从正常模式转化为饥饿模式,在饥饿模式下,mutex的所有权,从执行Unlock的goroutine转换到等待队列头部的Goroutine,后来者也不会自旋,也不会尝试获取锁,会直接到等待队列的尾部。

饥饿模式转换为正常模式

当一个等待着获得锁之后,它会在以下两种情况时将mutex从饥饿模式切换为正常模式

  • 它的等待时间小于1ms
  • 它是最后一个等待着

总结

正常模式,自旋和排队是同时存在的。

  • 优点

这样可以保证良好的吞吐量,因为频繁的挂起、唤醒goroutine会带来较多的开销,但是又不能无限制的自旋,要把自旋控制在一个较小的范围。

  • 缺点

可能会出现队列尾端的goroutine迟迟抢不到锁(尾端延迟的情况)

饥饿模式下,不再自旋尝试,所有goroutine统统都要排队

  • 优点

可以减少尾端延迟

  • 缺点

牺牲了吞吐性

lock和unlock的逻辑

https://cdn.cjpa.top/image-20210406142856743.png

state的类型时int32

第一位用作锁状态标识,置为1,表示已加锁,对应掩码常量位mutexLocked

第二位标记是否已经唤醒了,置为1,表示已经唤醒,对应掩码常量为mutexWoken

第三位标记mutex的工作模式,置为1,表示饥饿模式,对应掩码常量为mutextStaring

mutexWaiterShift=3,表示除了最低三位意外,state的其他位用来记录有多少个等待者在排队

https://cdn.cjpa.top/image-20210406143446521.png

lock和unlock方法

下面是精简掉了注释和race检测相关的代码

https://cdn.cjpa.top/image-20210406143528779.png

https://cdn.cjpa.top/image-20210406143533747.png

两个方法中主要通过atomic函数实现了fast path,相应的slowpath被单独放在了lockslow和unlockSlow方法中

https://cdn.cjpa.top/image-20210406143703750.png

源码注视中说:这样是为了便于编译器对内联fastpath进行优化

lock

lock方法的fastpath希望mutwx处于unlocked状态,没有goroutine在排队,更不会饥饿

https://cdn.cjpa.top/image-20210406144235986.png

理想状态下一个cas操作就可以获取锁

https://cdn.cjpa.top/image-20210406144246030.png

如果一个cas操作没有获取锁,就需要执行lockslow方法

unlock

unlock方法类似,需要用原子操作从mutex.state中减去mutexLocked(释放锁)

然后根据state的新值,来判断是否需要执行slowpath

https://cdn.cjpa.top/image-20210406144515453.png

如果新值为0,也就代表没有giroutine在排队,也就不需要执行其他操作,如果新值为0,就执行unlockslow,看看是不是需要唤醒某个giroutine

https://cdn.cjpa.top/image-20210406150451189.png