golang源代码阅读,sync系列-互斥锁Mutex


之前发布的 sync mutex golang主要是从场景触发,该篇主要是代码出发

总结

关键字:cas、semaphore(信号量)、自旋、饥饿状态

  1. Mutex 全称是mutual exclusion互斥,是golang的互斥锁的实现。golang的互斥锁,主要是使用cas和信号量来实现互斥,cas操作状态量-state,acquire和release操作信号量-sema。
  2. 进入Lock wait状态,一定是有一个goroutine已经进入了Lock,当前goroutine再进入才会被挂起。
  3. 大致过程为第一次调用Lock,会直接通过cas操作将state设置为mutexLocked状态,如果这时再有goroutine来进行Lock,则会进行自旋,如果满足的自旋条件的话,自旋完成后其它goroutine还未进行Unlock,则当前goroutine会尝试获取信号量,这时因为获取不到信号量,当前goroutine会被挂起阻塞,如果这时有之前的goroutine执行Unlock操作,则会release 信号量,并将阻塞的goroutine唤醒,执行完Lock函数,继续执行业务代码,执行完业务代码,Unlock锁,完成整个加解锁过程。

关键字说明

cas

compare and swap,用作设置state状态

semaphore

  1. 获取过程为将sema减一的过程,如果sema小于0,则将当前goroutine挂起
  2. 释放过程为将sema加一的过程,并将在获取信号量时被挂起的goroutine唤醒。
  3. 这篇文章不会讲述信号量具体实现,后面会规划单独一篇进行讲述。

自旋

为了减少goroutine的挂起、唤醒,先进行自旋,自旋不会将goroutine挂起来,只是进行cpu空转,后面会讲什么条件可以进入自旋

饥饿状态

为了防止有些waiter一直在等待,加入了饥饿状态。如果goroutine等待信号量超过1ms,会进入饥饿状态,下回Unlock唤醒,会直接将下一个waiter直接唤醒,具体在Unlock里面实现

代码注释说明

代码目录

代码入口在sync/mutex.go里面,struct为Mutex,并且定义了Locker的接口,接口为Lock和Unlock函数。Mutex实现了Locker接口。

结构体

type Mutex struct {
	state int32  // 存储状态码和当前的waiter个数
	sema  uint32 // 信号量
}

状态码

状态码使用int32表示,低三位分别表示三个状态,三位以上表示当前的waiter个数
状态码的定义很巧妙的使用的iota

	mutexLocked = 1 << iota // 被锁状态
	mutexWoken  //2  被锁状态,获取到信号量,当前goroutine会启动,进行执行代码
	mutexStarving //4 饥饿状态
	mutexWaiterShift = iota // 3位以上记录waiter个数

图结构表示如下
在这里插入图片描述

代码走读

Lock实现

func (m *Mutex) Lock() {
    // Fast path: grab unlocked mutex. 判断当前state是否为0,0为未上锁状态,通过cas操作实现,如果当前未Lock过直接设置为mutexLocked状态
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        return
    }
    // Slow path (outlined so that the fast path can be inlined) 当前未加锁成功,则进入慢加锁操作,对应解锁有unlockSlow
    m.lockSlow()
}

过程:进入该函数,都会先判断是否可以自旋,如果可以自旋,则先进行自旋,自旋完成如果还没有进入Unlock,则尝试获取信号量,默认sema等于0,会获取不到信号量,因而当前goroutine1被挂起
如果这时有其它goroutine0进行unlock,则会获取信号量成功(获取信号量,是按照队列的方式实现,先进先出,保证公平性)。获取信号量成功后,会进入mutexWakeup状态,并且这个状态会在循环的下一轮设置。进入下一轮for循环,会判断当前锁状态(old),如果状态是被解锁状态,则直接return,从而当前goroutine1完成加锁,继续执行业务代码。

func (m *Mutex) lockSlow() {
    var waitStartTime int64  // 等待时间
    starving := false  // 是否在饥饿状态
    awoke := false // 是否被唤醒
    iter := 0 // 自旋的次数
    old := m.state // 当前状态,整个过程存在old和new两个state量,old是当前循环state的状态,new为要被设置成的state
    for {
        // old&(11)  当前为mutexLocked状态 并且能自旋,则进入自旋状态,如果是starving状态,直接跳过
        if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
            // Active spinning makes sense.
            // Try to set mutexWoken flag to inform Unlock
            // to not wake other blocked goroutines.
            if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                awoke = true
            }
            // 进入自旋,实现为调用PAUSE指令,代码实现的地方在asm_amd64.s(这里是amd64架构为例)
            runtime_doSpin()
            iter++
            old = m.state
            continue
        }
        new := old
        // Don't try to acquire starving mutex, new arriving goroutines must queue.
        if old&mutexStarving == 0 {
            new |= mutexLocked
        }
        // 增加waiter数
        if old&(mutexLocked|mutexStarving) != 0 {
            new += 1 << mutexWaiterShift
        }
        // The current goroutine switches mutex to starvation mode.
        // But if the mutex is currently unlocked, don't do the switch.
        // Unlock expects that starving mutex has waiters, which will not
        // be true in this case.
        // starving是循环的上一轮结果赋值的,是在获取信号量后,计算挂起的时间超过1ms,会被设置为starving=true
        if starving && old&mutexLocked != 0 {
            new |= mutexStarving
        }
        // awoke也是循环的上一轮结果赋值的,在获取到信号量后
        if awoke {
            // The goroutine has been woken from sleep,
            // so we need to reset the flag in either case.
            if new&mutexWoken == 0 {
                throw("sync: inconsistent mutex state")
            }
            new &^= mutexWoken
        }
        // 当没有人改变state状态,跟old一样时,进入下面case,该出设置state
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 出口,退出lock,执行业务代码
            if old&(mutexLocked|mutexStarving) == 0 {
                break // locked the mutex with CAS
            }
            // If we were already waiting before, queue at the front of the queue.
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
            // 获取信号量
            runtime_SemacquireMutex(&m.sema, queueLifo, 1)
            // 计算是否超过1ms,是否设置为饥饿状态
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            old = m.state
            if old&mutexStarving != 0 {
                // If this goroutine was woken and mutex is in starvation mode,
                // ownership was handed off to us but mutex is in somewhat
                // inconsistent state: mutexLocked is not set and we are still
                // accounted as waiter. Fix that.
                if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
                    throw("sync: inconsistent mutex state")
                }
                delta := int32(mutexLocked - 1<<mutexWaiterShift)
                if !starving || old>>mutexWaiterShift == 1 {
                    // Exit starvation mode.
                    // Critical to do it here and consider wait time.
                    // Starvation mode is so inefficient, that two goroutines
                    // can go lock-step infinitely once they switch mutex
                    // to starvation mode.
                    delta -= mutexStarving
                }
                atomic.AddInt32(&m.state, delta)
                break
            }
            awoke = true
            iter = 0
        } else {
          // 如果其他goroutine已经修改state状态,重新进入for循环的下一轮
            old = m.state
        }
    }
}

自旋

说明:自旋是执行PAUSE指令,让cpu空转,不会将当前goroutine挂起
runtime_canSpin具体实现在runtime/pro.go里面的sync_runtime_canSpin。
mutex是并发式的,从而对于自旋的处理的会相对谨慎,只有满足下面的场景,才会进入自旋场景

  1. 自旋次数限制
  2. 是否多核
  3. p的本地q是否有goroutine
    返回false的场景:
  4. i超过4,即最多调用4次
  5. cpu数为1个
  6. gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1
  7. 当前p的本地队列有goroutine
    返回true的场景:
    其它场景返回都返回true
func sync_runtime_canSpin(i int) bool {
    // sync.Mutex is cooperative, so we are conservative with spinning.
    // Spin only few times and only if running on a multicore machine and
    // GOMAXPROCS>1 and there is at least one other running P and local runq is empty.
    // As opposed to runtime mutex we don't do passive spinning here,
    // because there can be work on global runq or on other Ps.
    if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
        return false
    }
    if p := getg().m.p.ptr(); !runqempty(p) {
        return false
    }
    return true
}

runtime_doSpin的具体实现
在runtime/proc.go里的sync_runtime_doSpin,里面调用procyield(active_spin_cnt),自旋次数为30次,具体实现为汇编代码,以amd64架构为例,
在asm_amd64.s里面,实际为调用PAUSE,即让cpu进行空转,不让出cpu调用

TEXT runtime·procyield(SB),NOSPLIT,$0-0
    MOVL    cycles+0(FP), AX
again:
    PAUSE
    SUBL    $1, AX
    JNZ again
    RET

Unlock解锁

func (m *Mutex) Unlock() {
    // Fast path: drop lock bit.
    new := atomic.AddInt32(&m.state, -mutexLocked)

    if new != 0 {
        // Outlined slow path to allow inlining the fast path.
        // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
        m.unlockSlow(new)
    }
    // 如果当前只有一个lock了,则直接返回
}
func (m *Mutex) unlockSlow(new int32) {
    if (new+mutexLocked)&mutexLocked == 0 {
        throw("sync: unlock of unlocked mutex")
    }
    if new&mutexStarving == 0 {
        old := new
        for {
            // 当前有多个waiter,但是未进入饥饿状态
            // If there are no waiters or a goroutine has already
            // been woken or grabbed the lock, no need to wake anyone.
            // In starvation mode ownership is directly handed off from unlocking
            // goroutine to the next waiter. We are not part of this chain,
            // since we did not observe mutexStarving when we unlocked the mutex above.
            // So get off the way.
            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                return
            }
            // Grab the right to wake someone.
            new = (old - 1<<mutexWaiterShift) | mutexWoken
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
                runtime_Semrelease(&m.sema, false, 1)
                return
            }
            old = m.state
        }
    } else {
        // 饥饿状态下:让下一个waiter先被调度到,释放出时间片,从而下一个waiter开始后能马上执行
        // Starving mode: handoff mutex ownership to the next waiter, and yield
        // our time slice so that the next waiter can start to run immediately.
        // Note: mutexLocked is not set, the waiter will set it after wakeup.
        // But mutex is still considered locked if mutexStarving is set,
        // so new coming goroutines won't acquire it.
        runtime_Semrelease(&m.sema, true, 1)
        // 释放信号量,实则让sema+1,从而waiter能acquire信号量
    }
}

感想

golang的互斥锁实现很精致,涉及的点比较多,有cas、信号量、自旋、饥饿等,使用32位的int32表示lock、awake、starving和waiter个数很独特(个人而言,类似实现在go源码中很多,如WaitGroup等)。当前阅读源码,只是了解大致过程,对与细小的点还得后续继续阅读源码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值