【go语言之lock源码分析】

写在前面

这个锁就是保证go线程安全的方法,通过加锁的方法实现,主要分为lock和unlock方法。

然后看一下对应的结构体。

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
//
// In the terminology of the Go memory model,
// the n'th call to Unlock “synchronizes before” the m'th call to Lock
// for any n < m.
// A successful call to TryLock is equivalent to a call to Lock.
// A failed call to TryLock does not establish any “synchronizes before”
// relation at all.
type Mutex struct {
	state int32 
	sema  uint32
}

state 是32位数字,前三位分别是,是否加锁,是否有goroutine被唤醒,已经当前Mutex的模式,是正常模式还是饥饿模式,对应的源码中的是

	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving

剩下的都是当前排队的数量。
sema 是信号量,用来进行排队,涉及到go的底层

然后看一下锁的饥饿模式和正常模式,当然这个在后面的lock的源码中也会说,先看一下官方的解释

	// Mutex can be in 2 modes of operations: normal and starvation.
	// In normal mode waiters are queued in FIFO order, but a woken up waiter
	// does not own the mutex and competes with new arriving goroutines over
	// the ownership. New arriving goroutines have an advantage -- they are
	// already running on CPU and there can be lots of them, so a woken up
	// waiter has good chances of losing. In such case it is queued at front
	// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
	// it switches mutex to the starvation mode.
	//
	// In starvation mode ownership of the mutex is directly handed off from
	// the unlocking goroutine to the waiter at the front of the queue.
	// New arriving goroutines don't try to acquire the mutex even if it appears
	// to be unlocked, and don't try to spin. Instead they queue themselves at
	// the tail of the wait queue.
	//
	// If a waiter receives ownership of the mutex and sees that either
	// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
	// it switches mutex back to normal operation mode.
	//
	// Normal mode has considerably better performance as a goroutine can acquire
	// a mutex several times in a row even if there are blocked waiters.
	// Starvation mode is important to prevent pathological cases of tail latency.

简单翻译一下就是
在正常模式下的锁,是FIFO也就是先进先出,但是在这种情况下面,在队列中的goroutine中会和新来的goroutine去竞争,但是因为新来的goroutine不需要上下文切换和已经占了cpu,所以会大概率获到锁,导致排队中的goroutine拿不到锁。
当一个gouroutine等到时间超过1ms的时候,导致整个锁处于一个饥饿模式中,这会导致新来的goroutine不会再和队列中的去竞争,而是会在队列后面加入。而当队列中只有最后一个goroutine和等待的时间少于1ms的时候,会重新从饥饿模式切换到正常模式。

看一下锁操作的几个方法

lock

这个lock还分为快加锁和慢加锁。

fast lock

// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

可以看出来这个Fast Lock其实就是一个简单的CAS的操作,用来去获取锁,成功就返回,没成功就进入到slow path。

slow path

这里主要还是和官方的源码注释结合在一起来看

func (m *Mutex) lockSlow() {
    // 声明变量
  
    // goroutine的等待时间
	var waitStartTime int64
    
    // 是否是饥饿模式
	starving := false
  
    // 是否被唤醒
	awoke := false
 
    // 自旋的次数
	iter := 0
  
    // 保存的旧的状态值
	old := m.state
	for {
		// Don't spin in starvation mode, ownership is handed off to waiters
		// so we won't be able to acquire the mutex anyway.
		// 先判断是否是饥饿模式,因为饥饿模式下,直接让goroutine去等待了 然后是否还需要自旋,超过了30次不再自旋
		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.
			// 如果当前不是唤醒状态 而且整个锁没有唤醒状态 并且等待的队列超过1,并且可以更改m.state的状态 那么设置awoke为true
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			// 做自旋
			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
		}
		// 当前模式为加锁或者饥饿模式 那么排队数量+1
		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在下面会设置
		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
		}
		// 尝试去更改状态
		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是否之前已经等待
			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)
				// 如果不是饥饿模式 或者只有一个等待的goroutine 切换到正常模式
				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
		}
	}

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

从上面可以看出,首先会进行自旋,然后根据一系列条件设置新的状态,然后去抢锁,抢到返回,抢不到那么就等待去沉睡,被唤醒之后再次进入循环去抢锁。

Unlock

和lock一样,Unlock也分快和慢,当然原理也是类似的

fast unlock

// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// 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)
	}
}

可以看出来只是通过在state中减去mutexLocked这个值,如果为0说明解锁成功,并且没有其他等待线程,直接返回。

slow path

这个还是通过官方源码结合注释看一下

func (m *Mutex) unlockSlow(new int32) {
    // 如果本身就是未解锁 那么排除异常
	if (new+mutexLocked)&mutexLocked == 0 {
		fatal("sync: unlock of unlocked mutex")
	}
	// 判断是否是饥饿模式
	// 如果是饥饿模式那么直接从队列中唤醒一个
	if new&mutexStarving == 0 {
	   
		old := new
		for {
			// 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.
			// 如果当前没有等待着 或者已经加锁 处于饥饿模式 或者有goroutine处于唤醒状态 直接返回
		
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// 进行CAS操作 唤醒一个 否则进入下一个循环
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		// 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)
	}
}

可以看出来 解锁的核心方法就是runtime_Semrelease,也就是在goroutine的等待队列中唤醒一个等待者,然后唤醒以后,等待者尝试去获得锁,判断饥饿模式等等,也就是上面的lock方法的逻辑。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值