目录

Go 基础知识与框架体系 系列八: mutex 互斥锁

这篇文章总结了 Go 的知识体系: mutex,包括其中的底层实现等等。

mutex 互斥锁

1. mutex 底层结构

mutex 底层结构如下:

1
2
3
4
type Mutex struct {
	state int32
	sema uint32
}
  • 零值就是一个有效的互斥锁,处于Unlocked状态。
  • state存储的是互斥锁的状态,加锁和解锁,都是通过atomic包提供的函数原子性,操作该字段。
  • sema用作一个信号量,主要用于等待队列。

Mutex有两种模式,在正常模式下,一个尝试加锁的goroutine会先自旋四次,自旋锁(如果不成功就一直尝试),尝试通过原子操作获得锁,若几次自旋之后仍不能获得锁,则通过信号量排队等待。其中,所有等待者会按照先入先出FIFO的顺序排队。

/mutex.png
图1:mutex正常模式

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

/mutex2.png
图2:mutex饥饿模式

而当一个goroutine本次加锁等待时间超过了1ms后,它会把当前Mutex从正常模式切换至“饥饿模式”。

在饥饿模式下,Mutex的所有权从执行Unlock的goroutine,直接传递给等待队列头部的goroutine,后来者不会自旋,也不会尝试获得锁,即使Mutex处于Unlocked的状态。它们会直接到队列的尾部排队等待。

/mutex3.png
图3:mutex饥饿模式下的goroutine队列

当一个等待者获得锁之后,它会在以下两种情况时,将Mutex由饥饿模式切换回正常模式。

  • 第一种情况是它的等待时间小于1ms,也就是它刚来不久
  • 第二种情况是它是最后一个等待者,等待队列已经空了,后面自然就没有饥饿的goroutine了
注意
综上所述,在正常模式下自旋和排队是同时存在的,执行lock的goroutine会先一边自旋,尝试4次后如果还没拿到锁,就需要去排队等待了,这种排队之前先让大家来抢的模式,能够有更高的吞吐量,因为频繁的挂起,唤醒goroutine会带来较多的开销。但是又不能无限制的自旋,要把自旋的开销控制在较小的范围内,所以在正常模式下,Mutex有更好的性能。 但是可能会出现队列尾端的goroutine迟迟抢不到锁(尾端延迟)的情况。

/mutex4.png
图4:mutex饥饿模式下的goroutine不再自旋

而饥饿模式不再尝试自旋,所有goroutine都要排队,严格的FIFO,对于防止出现尾端延迟来讲特别重要。