源码解析
面试官:请你介绍下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
        }
    }
}