Go互斥锁sync.Mutex的实现细节

基本知识

获取锁的方案

对于获取锁,一般来讲有两种方案,一种是不断地自旋+CAS,另一种就是阻塞+唤醒。两种方式各有优劣。Go语言结合了这两种方案,自动的判断当前锁的竞争情况,先尝试自旋几次,如果锁一直没被释放,再加入阻塞队列。

公平性

Go语言的锁存在两种模式:正常模式和饥饿模式。
正常模式:阻塞的协程进入FIFO的阻塞队列,所有协程对锁进行抢占式调度。也就是说,当某个协程从阻塞队列被唤醒时,它并不能直接获得锁,而是还要自己再去抢占一次(此时可能有其他协程刚刚进入Lock()流程,还在自旋尝试,没有进入阻塞队列)。源码的注释中提到,在这种模式下,从阻塞队列被唤醒的协程处于劣势。因为新来的协程已经在占用CPU,并且可能数量很多。
饥饿模式:上面提到从阻塞队列唤醒的协程在竞争上处于劣势,有可能某协程一直在阻塞队列,得不到锁。为了避免这种情况,当某协程在阻塞队列阻塞了很长一段时间(1ms)后,会将锁设置为饥饿模式。在饥饿模式下,想要获得锁的所有协程不会自旋等待,而是直接进入阻塞队列尾部去排队。

Mutex结构体

type Mutex struct {
    state int32
    sema  uint32
}

const (
    mutexLocked = 1 << iota // mutex is locked
    mutexWoken
    mutexStarving
    mutexWaiterShift = iota
    starvationThresholdNs = 1e6 //1ms
)

state表示了该锁的所有信息,包括mutexLocked(是否已上锁),mutexWoken(是否有在运行的协程在尝试抢占锁,便于Unlock的时候判断是否要从阻塞队列唤醒协程), mutexStarving(是否处于饥饿模式),以及在阻塞队列的协程数量。

state的bit布局如下:


|31-----------------3|-------2-----|-----1----|-----0-----|
|阻塞队列中的协程数量|mutexStarving|mutexWoken|mutexLocked|

Lock()

下面来看一下Lock方法具体的实现。

func (m *Mutex) Lock() {
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		
		......
        return
    }
    // Slow path (outlined so that the fast path can be inlined)
    m.lockSlow()
}

第一个if条件表示,如果锁是一把全新(没有其他协程动过)的锁,直接锁上就行了,然后返回。否则进入lockSlow()流程。

lockSlow()

func (m *Mutex) lockSlow() {
    var waitStartTime int64
    starving := false
    awoke := false
    iter := 0
    old := m.state

	//...
}

局部变量的意义:
waitStartTime:在阻塞队列中等待的总时长,这个值是会累加的。
starving:标记是否要将锁改为饥饿模式。
awoke:标记是否要修改锁的mutexWoken位。
iter:尝试自旋的次数。
old:锁的老状态,用于CAS操作等。

    for {
        // Don't spin in starvation mode, ownership is handed off to waiters
        // so we won't be able to acquire the mutex anyway.
        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
            }
            runtime_doSpin()
            iter++
            old = m.state
            continue
        }
        
        //...
   }

1、先判断锁的状态,要锁已经被占用、不为饥饿模式(饥饿模式不能抢占,要去阻塞队列排队)、能继续自旋才进入第一个if的逻辑。
2、假如当前锁mutexWoken为0,即没有其他协程在运行并等待,并且阻塞队列的大小不为0,就可以将mutexWoken标记为1。

	for{
		//...
		
        new := old
        // Don't try to acquire starving mutex, new arriving goroutines must queue.
        if old&mutexStarving == 0 {
            new |= mutexLocked
        }

        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.

        if starving && old&mutexLocked != 0 {
            new |= mutexStarving

        }

        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,准备获取锁了。
1、如果old的mutexStarving为0,说明当前协程可以加锁,在new中写入mutexLocked位为1。
2、如果old的mutexLocked或者mutexStarving为1,当前协程肯定不能加锁,阻塞队列的值加1。
3、如果当前协程为starving状态(在阻塞队列中等了太长时间)并且old的状态为锁定,将new的mutexStarving位写为1。
4、如果当前协程为awoke状态,需要修改new的mutexWoken位的值,写为0(后续当前线程只有获取锁和阻塞两种可能)。

        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            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)
            //被唤醒
            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 {
            old = m.state
        }
    }

这里为尝试更新锁状态,如果CAS操作失败,则更新old的值后继续循环。
如果成功:
1、如果old未被锁定且不为饥饿模式,说明当前协程加锁成功,函数可以返回。
2、判断当前协程是刚来还是从阻塞队列中醒来,如果是刚来,放到阻塞队列末尾,否则放到阻塞队列头部。
3、runtime_SemacquireMutex(&m.sema, queueLifo, 1)表示放入阻塞队列并阻塞。
4、被唤醒后更新starving的值。
5、如果当前锁是饥饿模式,则当前协程可以直接获取锁, 否则又回到循环,尝试抢占。
6、获取锁时要更新mutexLocked位、阻塞队列的大小。如果当前协程不在starving状态或者阻塞队列中只有一个协程,将锁改为正常模式。

Unlock()

比较简单,暂时省略。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值