Skip to main content

源码解析

面试官:请你介绍下Mutex Lock做了什么事情...

前言:

  • Go版本:v1.20

不知道你有没有遇到过这样的面试场景:

  • 面试官:你了解原生库sync
  • 小白:了解了解,工作中使用场景很多
  • 面试官:有用过sync.Mutex没
  • 小白:用的用的,Go中的互斥锁嘛,并发场景下用的很多的,比如巴拉巴拉...
  • 面试官(微笑):好的,看过Mutex源码没,请介绍一下实现原理
  • 小白:???

出门右转后...
找到陈大哥...

  • 小白:可恶啊,Mutex不就只需要Lock、Unlock就行了吗,我需要懂锤子啊我丢,陈大哥快安慰我...
  • 陈大哥:在正常业务中,Mutex只需要锁住之后,在延迟函数中释放即可,你没有错...
  • 小白:呜呜呜...
  • 陈大哥:但在Go中,Mutex并不是Lock之后就直接阻塞等待,而是做了很多事情的...
  • 小白:你先说,我保证听不懂...
  • 陈大哥:...

Go Mutext Lock主要做了三件事情:

  1. 使用原子锁尝试加锁。
  2. 使用自旋锁尝试加锁。
  3. 使用信号锁等待加锁。

在Go中,Mutex数据结构只有两个字段,分别是:state、sema

type Mutex struct {
state int32 // 表示锁状态:1:加锁状态、2:唤醒状态、4:饥饿状态
sema uint32 // 表示用来控制等待者队列的信号量
}

const (
mutexLocked = 1 // 加锁状态
mutexWoken = 2 // 唤醒状态
mutexStarving = 4 // 饥饿状态
)

下面分别从对应源码来分析各个阶段的场景,为了更好的理解,源码中仅提炼部分关键信息。

使用自旋锁尝试加锁

func (m *Mutex) Lock() {
// 用原子库atomic尝试给自己的state换状态,也就是上锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 尝试加锁失败,进入下一步
m.lockSlow()
}

使用自旋锁尝试加锁

func (m *Mutex) lockSlow() {
var waitStartTime int64 // 记录等待开始的时间戳
starving := false // 标记锁是否处于饥饿状态
awoke := false // 标记是否已经唤醒其他被阻塞的goroutine
iter := 0 // 记录自旋的次数
old := m.state // 保存当前锁的状态
for {
// 当锁处于饥饿状态下时,放弃自旋锁
// 当自旋次数iter大于等于4时,放弃自旋
// 否则,开始自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 主动自旋是有意义的。
// 尝试设置mutexWoken标志,通知Unlock
// 不要唤醒其他被阻塞的goroutine。
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin() // 执行自旋操作
iter++ // 自旋次数加1
old = m.state // 更新锁的状态
continue
}
}
}