go语言sync.Mutex

go语言sync.mutex

数据结构

type Mutex struct {
	state int32
	sema  uint32
}

Mutex包含了两个字段,分别是state、sema,state表示了当前锁的状态,sema是用于控制锁的一个信号量。这是一个零值可用的结构体,零值表示未加锁

state 字段的最低三位表示三种状态,分别是 mutexLocked mutexWoken mutexStarving ,剩下的用于统计当前在等待锁的 goroutine 数量

  • mutexLocked 表示是否处于锁定状态
  • mutexWoken 表示是否有goroutine被唤醒
  • mutexStarving 表示是否处于饥饿状态
  • waiterShift 表示等待的goroutine数量

Lock

func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        // 用于辅助race检测器进行手动竞态检查
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

Lock方法首先进行了一次cas操作,尝试将state的值从0改为1,代表首次进行加锁,如果成功直接返回,若是不成功,将调用lockSlow方法进行加锁。

func (m *Mutex) lockSlow() {
	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.
		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
		}
		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
		}
		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
		}
	}

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

lockSlow流程:

  1. 在一个for循环中首先进行自旋判断,将会对以下条件进行判断:
    • 当前互斥锁的状态是非饥饿状态,并且已经被锁定了
    • 自旋次数不超过 4 次
    • cpu 个数大于一,必须要是多核 cpu
    • 当前正在执行当中,并且队列空闲的 p 的个数大于等于一
  2. 进入自旋后将会尝试将state设置位唤醒状态,可以防止当前持有锁的协程唤醒其他的协程
  3. 设置完woken状态就会调用 runtime_doSpin 方法进入自旋, doSpin 方法会调用 procyield(30) 执行三十次 PAUSE 指令
  4. 迭代自旋次数
  5. 当自旋结束或者不能自旋的话将old的值赋值给new
    1. 如果old不是饥饿状态的话,new设置为加锁状态
    2. 如果old属于锁定状态或者饥饿状态的话,new设置等待的goroutine数量加1
    3. 如果当前协程将锁切换为饥饿状态,并且old属于锁定状态,new设置为饥饿状态
    4. 如果当前协程属于被唤醒状态,将唤醒标志位清零
  6. 通过cas方法将state的值设置为new,如果成功流程继续,如果失败,将old重新赋值
    1. 如果old不属于饥饿状态且不属于锁定状态,说明当前协程通过这次cas操作获取了锁,直接break
    2. 根据wait时间判断是否是新来的协程,如果是的话加入到等待队列末尾,不是的话保持在队首
    3. 调用runtime_SemacquireMutex方法将当前协程挂起,如果协程被唤醒了,再进行后面的流程
    4. 协程再次被唤醒后,计算等待时间是否超过1ms,如果超过,更新饥饿状态,并获取state的值
    5. 判断当前锁是否处于饥饿状态,如果是的话,说明当前协程是从饥饿状态被唤醒的,那么可以进行加锁操作,如若不是将唤醒状态改为true,循环次数重置
    6. 通过原子操作将state设置为加锁状态,并且将等待数量减1,如果当前协程不需要设置锁为饥饿状态或者自身为队列中最后一个等待的协程,那么还要解除锁的饥饿状态

Unlock

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的锁定状态,如果state不等于0,也就是说还有等待的协程或者还处于饥饿状态,那么调用unlockSlow方法进行解锁

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

unlockSlow流程:

  1. 如果传进来的解锁后的state加上锁定状态还是处于未加锁状态,直接报错
  2. 判断当前state是否是饥饿状态,如果是的话直接唤醒等待的协程,不是的话执行后续流程
  3. 如果state当前没有协程等待或者处于锁定状态或者唤醒状态或者饥饿状态其中一个的话,直接返回
  4. 原子操作将state设置为唤醒状态,并且减去一个等待的协程,并且唤醒等待的协程

总结

  1. 通过一个int32类型的变量,并且不同位来表示不同的状态参数,并且运用原子操作来进行更改锁的状态
posted @   每天提醒自己要学习  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
点击右上角即可分享
微信分享提示