锁是目前所有并发程序中用于保证程序执行时序和一次性的重要机制,用于控制临界资源的访问和更新在抢占式业务中间的正确性。
mutex表示互斥的意思,是 mutual exclusion 的组合表达.
一、Go 的锁结构
go中的锁结构分为互斥锁(Mutex)和读写锁(RWMutex)。
以互斥锁为例:
type Mutex struct {
state int32 // 锁的标志位
sema uint32 // 信号量,表示等待队列和等待信息
}
一个默认的Mutex结构就是一个完整的锁结构。
var lock sync.Mutex
state = 0 => 表示当前是未加锁的状态
sema = 0 => 表示当前锁的一些状态信息
锁的两种模式
-
正常模式
正常模式下自旋和队列等待是同时存在的。有强锁的机制可以保证更高的吞吐量。 正常模式下的mutex有更好的性能,但是可能出现队尾的goroutine一直抢不到锁的情况,也称为尾端延迟。
-
饥饿模式
mutex的饥饿模式不在自旋尝试,所有的goroutine都需要排队,严格按照FIFO的方式获得锁。
多并发加锁模型
正常模式下,对于并发的场景,一个尝试加锁的groutine会先自旋几次,尝试通过原子操作获得锁。如果几次自旋之后还是不能获得锁,则通过信号量(sema)进行排队等待,所有的等待者都是按照先入先出(FIFO)的顺序排队。
但是当G获取锁之后将锁释放,此时的G1是最先排入等待队列的goroutine,此时它会被唤醒。但是被唤醒的G1并没有直接获取锁的资格,而是需要和此时还未进入排队等待中的自旋的goroutine们竞争。
相比之下,刚刚被唤醒的G1和现在被调度运行中的goroutine们,未在队列中间的协程更有优势,因为刚被唤醒的Goroutine只会有一个,而且处于自旋中间的goroutine可能有很多个。所以G1获得锁的概率会变得比较低。
如果刚被唤醒的G1未获得锁,则它将重新被加入等待队列中,但是它仍旧会保持在当前等待队列的头部。
正常模式切换为饥饿模式
如果重新回入等待队列中间的G1在本次尝试加锁的等待时间超过1ms之后,他会把当前的mutex从正常模式切换到饥饿模式**。**
在饥饿模式下,锁的竞争模式和正常模式不同。饥饿模式下,G(n)释放锁之后,将不会再存在G1和当前的自旋中的goroutine们一起竞争锁的情况。G1是当前等待队列的头部元素,同时也是Gn的直接后继。自旋中的goroutine们会被插入到当前的等待队列的尾部。
饥饿模式切换回正常模式
饥饿模式下,所有等待中的goroutine将被规整到等待队列中,会严格执行先到先得的顺序保证公平,但是饥饿模式下调度不会有很高的吞吐量,所以在如下两种情况会有将当前的mutex从饥饿模式切回正常模式。
第一种情况是,获得锁的当前goroutine总的等待时间小于1ms。当前的goroutine会将当前mutex的模式位置标记为正常。
第二种情况是当前的剩余等待队列为空,无其他的等待者,此时mutex的模式位置也会被置为正常模式。
概念有点多写的有点长了,尽量深入浅出,准备分三篇来详解,喜欢的继续追下去吧,嘿嘿