文章目录

  • GoLang之Mutex底层系列一(锁、正常模式、饥饿模式)


GoLang之Mutex底层系列一(锁、正常模式、饥饿模式)

Mutex这个名称的由来,应该是Mutual exclusion的前缀组合,俗称互斥体或者互斥锁。
Go中Mutex的数据结构如下这样的,因为足够简单,所以不需要额外的初始化,此结构的零值就是一个有效的互斥锁,处于Unlocked状态。state存储的是互斥锁的状态,加锁和解锁都是通过atomic包提供的函数原子性来操作该字段。sema用作一个信号量,主要用于等待队列。

Ymodem协议库 mute协议_golang


Ymodem协议库 mute协议_后端_02

Mutex有两种模式,在正常模式下,一个尝试加锁的goroutine会先自旋几次,尝试通过原子操作获得锁,

Ymodem协议库 mute协议_开发语言_03

若几次自旋之后仍不能获得锁,则通过信号量排队等待。
所有等待者会按照先入先出FIFO的顺序排队。

Ymodem协议库 mute协议_开发语言_04

但是当锁被释放,第一个等待者被唤醒后并不会直接拥有锁,而是需要和后来者竞争,也就是那些处于自旋阶段,尚未排队等待的goroutine。这种情况下后来者更有优势,一方面,它们正在CPU上运行,自然比刚被唤醒的goroutine更有优势,另一方面处于自旋状态的goroutine可以有很多,而被唤醒的goroutine每次只有一个,所以被唤醒的goroutine有很大概率拿不到锁。这种情况下它会被重新插入到队列的头部,而不是尾部。

Ymodem协议库 mute协议_后端_05


Ymodem协议库 mute协议_Ymodem协议库_06


Ymodem协议库 mute协议_Ymodem协议库_07

而当一个goroutine本次加锁等待时间超过了1ms后,它会把当前Mutex从正常模式切换至“饥饿模式”。
在饥饿模式下,Mutex的所有权从执行Unlock的goroutine,直接传递给等待队列头部的goroutine,后来者不会自旋,也不会尝试获得锁,即使Mutex处于Unlocked的状态。它们会直接到队列的尾部排队等待。

Ymodem协议库 mute协议_互斥锁_08


Ymodem协议库 mute协议_golang_09

当一个等待者获得锁之后,它会在以下两种情况时,将Mutex由饥饿模式切换回正常模式。
第一种情况是它的等待时间小于1ms,也就是它刚来不久

Ymodem协议库 mute协议_Ymodem协议库_10

第二种情况是它是最后一个等待者,等待队列已经空了,后面自然就没有饥饿的goroutine了

Ymodem协议库 mute协议_Ymodem协议库_11

综上所述,在正常模式下自旋和排队是同时存在的,执行lock的goroutine会先一边自旋,尝试几次后如果还没拿到锁,就需要去排队等待了,这种排队之前先让大家来抢的模式,能够有更高的吞吐量,因为频繁的挂起,唤醒goroutine会带来较多的开销。但是又不能无限制的自旋,要把自旋的开销控制在较小的范围内,所以在正常模式下,Mutex有更好的性能。 但是可能会出现队列尾端的goroutine迟迟抢不到锁(尾端延迟)的情况。
而饥饿模式不再尝试自旋,所有goroutine都要排队,严格的FIFO,对于防止出现尾端延迟来讲特别重要。

Ymodem协议库 mute协议_Ymodem协议库_12