go sync.Mutex 源码解析

本文详细解析了go语言中sync.Mutex的实现,包括Locker接口、Mutex常量、锁的操作模式(正常和饥饿)、Mutex结构体、Lock和Unlock方法以及内部涉及的runtime*函数,探讨了在不同模式下锁的行为及其性能考虑。

目录

一、sync.Mutex是什么?

Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。
 

Mutex在第一次使用后不能被复制.

二、sync.Locker 接口

        type Locker interface{
            Lock()
            Unlock()
        }

三、sync.Mutex 常量

const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving
	mutexWaiterShift = iota

	starvationThresholdNs = 1e6
)

mutexLocked:锁定状态,值为1,二进制:0001
mutexWoken:唤醒状态,值为2,二进制:0010

mutexStarving:饥饿状态,值为4,二进制:0100

mutexWaiterShift:等待goroutine偏移量,值为3

starvationThresholdNs:进入饥饿模式,等待纳秒数,等于1ms

四、 Mutex操作模式:正常和饥饿

在正常模式下,等待者按照FIFO顺序排队,但是唤醒的等待goroutine并不拥有互斥锁,而是与新到达的goroutines争夺互斥锁的所有权。新加入的goroutines有一个优势——它们已经在CPU上运行,在获取不到锁的情况下,可以进行自旋,尝试获得锁,所以一个唤醒的等待者很有可能会输。在这种情况下,它被排在等待队列的前面。

在饥饿模式下,互斥锁的所有权直接从正在解锁的goroutine传递给队列前面的等待goroutine。新的goroutine不会试图获取互斥锁,即使它看起来已经解锁,也不会试图旋转。相反,他们把自己排在等待队列的尾部。

正常模式与饥饿模式之间的切换:

如果一个等待goroutine获得互斥锁的时间超过1ms,它将互斥锁切换到饥饿模式。
 

如果一个等待goroutine收到互斥锁的所有权,并且它是队列中的最后一个等待者,或者它等待的时间少于1毫秒,它会将互斥锁切换回正常操作模式。

** 普通模式的性能要好得多,因为即使有阻塞的等待器,goroutine也可以连续多次获取互斥锁。饥饿模式对预防尾潜伏期的病理病例有重要意义。

五、sync.Mutex 结构体

type Mutex struct {
	state int32
	sema  uint32
}

state,长度为32为的整数,通过位操作锁状态、等待者个数:
       前29位表示等待goroutine的数量,获取方式:Mutex.state >> mutexWaiterShift
       第30位表示是否是饥饿模式,判断依据Mutex.state & mutexStarving,1-饥饿模式,0-正常模式。
       第31位表示是否被唤醒,判断依据Mutex.state & mutexWoken,1-正在被唤醒 0-未被唤醒。
       第32位表示是否被锁定,判断依据Mutex.state & mutexLocked,1-锁定 0-未锁定。

sema:信号量,用于在获取锁/释放锁时,进入睡眠/被唤醒。

六、Lock方法

func (m *Mutex) Lock() {
	// 快速路径:抓取未锁定的互斥锁。
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// 慢路径
	m.lockSlow()
}

func (m *Mutex) lockSlow() {
	var waitStartTime int64 //当前goroutine的等待时间
	starving := false //当前goroutine是否处于饥饿模式
	awoke := false //当前goroutine是否是唤醒状态
	iter := 0 //迭代次数
	old := m.state //获取锁信息
	for {
		//在正常模式下,通过runtime_canSpin方法判断是否可以进入cpu自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			//尝试进入唤醒状态
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			runtime_doSpin()//自旋
			iter++  //更新迭代次数
			old = m.state //更新锁信息
			continue
		}
        // 更新state
		new := old
		// 判断是否是正常模式,若是则更新new的锁定状态
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
        // 当前锁的状态是已加锁且是饥饿模式,当前goroutine进行排队,并更新等待者数量
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		// 当前goroutine处于饥饿模式,且锁是加锁状态,更新new为饥饿模式
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
        // 当前goroutine处于唤醒状态
		if awoke {
			// 当前锁若不是唤醒状态,则panic
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			new &^= mutexWoken
		}
        //通过CAS更新state
		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) // 通过信号阻塞当前goroutine,等待唤醒
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			if old&mutexStarving != 0 { //饥饿模式
				// 如果当前state已锁定,已唤醒或等待者为0,则panic
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				delta := int32(mutexLocked - 1<<mutexWaiterShift)// 等待者goroutine数量减一
				if !starving || old>>mutexWaiterShift == 1 { //若当前goroutine非饥饿模式或等待者数量为1,则切换正常模式
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)//更新state
				break
			}
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}

七、Unlock

//互斥锁解锁

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// 快速解锁
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {//存在其它等待goroutine
		m.unlockSlow(new)
	}
}

func (m *Mutex) unlockSlow(new int32) {
    // 判断是否处于未加锁状态,在未加锁状态下进行解锁,会panic
	if (new+mutexLocked)&mutexLocked == 0 { 
		throw("sync: unlock of unlocked mutex")
	}
	if new&mutexStarving == 0 {//正常模式
		old := new
		for {
			//若是有等待goroutine或已经有加锁、唤醒、饥饿时,则返回
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// 等待goroutine个数减一,且设置唤醒状态
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
                // 通过信号唤醒一个goroutine,但不是第一个
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		// 饥饿模式下,通过信号唤醒第一个等待goroutine
		runtime_Semrelease(&m.sema, true, 1)
	}
}

八、Mutex中使用的runtime*

runtime_canSpin:

runtime_canSpin对应runtime/proc.go 的sync_runtime_canSpin方法,用于判断是否可以进行自旋操作

// sync.Mutex 的主动自旋
//go:linkname sync_runtime_canSpin sync.runtime_canSpin
//go:nosplit
func sync_runtime_canSpin(i int) bool {
	// 若迭代次数超过active_spin(4),或 cpu核数为1,或 逻辑处理器>1,返回false
	if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
		return false
	}
    //若逻辑处理器的本地goroutine队里为空,返回false
	if p := getg().m.p.ptr(); !runqempty(p) {
		return false
	}
	return true
}

runtime_doSpin:

runtime_doSpin对应runtime/proc.go 的sync_runtime_doSpin方法,用于自旋操作

runtime_SemacquireMutex:

runtime_SemacquireMutex对应 runtime/sema.go 的sync_runtime_SemacquireMutex方法,通过信号阻塞goroutine,在lockSlow中使用.

runtime_Semrelease:

runtime_Semrelease对应 runtime/sema.go 的 sync_runtime_Semrelease 方法,通过信号唤醒阻塞的goroutine.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值