源码解析
面试官:请你介绍下Mutex Lock做了什么事情...
前言:
- Go版本:v1.20
不知道你有没有遇到过这样的面试场景:
- 面试官:你了解原生库sync
- 小白:了解了解,工作中使用场景很多
- 面试官:有用过sync.Mutex没
- 小白:用的用的,Go中的互斥锁嘛,并发场景下用的很多的,比如巴拉巴拉...
- 面试官(微笑):好的,看过Mutex源码没,请介绍一下实现原理
- 小白:???
出门右转后...
找到陈大哥...
- 小白:可恶啊,Mutex不就只需要Lock、Unlock就行了吗,我需要懂锤子啊我丢,陈大哥快安慰我...
- 陈大哥:在正常业务中,Mutex只需要锁住之后,在延迟函数中释放即可,你没有错...
- 小白:呜呜呜...
- 陈大哥:但在Go中,Mutex并不是Lock之后就直接阻塞等待,而是做了很多事情的...
- 小白:你先说,我保证听不懂...
- 陈大哥:...
Go Mutext Lock主要做了三件事情:
- 使用原子锁尝试加锁。
- 使用自旋锁尝试加锁。
- 使用信号锁等待加锁。
在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
}
}
}