源码分析-Golang Mutex

锁的实现

数据结构

type Mutex struct {
   state int32
   sema  uint32
}
  • 一个mutex 总共只占8个字节,因此是一个充分使用位的数据结构

  • state意义:

    • 最后3位充当状态的概念
      • 第0位: mutexLocked: 表示互斥锁的状态为锁定状态,既是否被某个goroutine所持有,是否已经被加锁
      • 第1位: mutexWoken: 是否被唤醒(既某个goroutine尝试获取锁)
      • 第2位: mutexStarving:表示当前的互斥锁处于 饥饿模式
      • 剩下的则是有多少个goroutine 在等待互斥锁的释放
      • 在这里插入图片描述
  • sema意义:

    • 充当信号量:用来唤醒goroutine

正常模式和饥饿模式

  • 正常模式:

    • 锁的等待者会按先进先出的顺序获取锁
      • 唤醒的goroutine不会直接获取锁,而是和新请求的routine竞争获取锁,因为新请求的goroutine 占用着cpu,所以刚刚唤醒的routine大概率会失败竞争
  • 饥饿模式:

    • 锁直接交给goroutine 等待队列中的第一个,而不会触发竞争(即时锁看上去处于unlock状态,依旧不会自旋,而是直接放入等待队列中)

    • 作用:

      • 防止goroutine 被饿死,既因为刚请求的goroutine更大概率获得锁,意味着之前的goroutine 很可能一直获取不到锁

源码分析:

hard code 定义分析

const (
	mutexLocked = 1 << iota // 0001 ,代表的是,这个锁已经被加锁
	mutexWoken  0010 代表的是,是否有routine被唤醒
	mutexStarving    0100 代表的是,当前锁处于饥饿模式
	mutexWaiterShift = iota   等待的goroutine数的位移, 如 stat>>mutexWaiterShift 就能得到当前的wait数

	starvationThresholdNs = 1e6 // 1e6 ns =1ms ,既 如果当前goroutine在1ms 内获取得到锁,并且处于饥饿模式时,将锁
	更正为正常模式
)

一些native 函数解释

  • sync_runtime_canSpin: 自旋
    • 自旋只会自旋几次,4次
    • 运行于多核的机器上
    • 当前机器上至少存在一个正在运行的P,并且处理的运行队列为空
  • runtime.sync_runtime_doSpin
    • 执行PAUSE 指令,空占CPU时间
  • sync_runtime_SemacquireMutex:
    • 通过golang 运行时的信号量,获取信号量
    • lifo:如果为true 则会排在队头,
  • runtime_Semrelease
    • 通过golang的信号量,释放通知
    • handoff: 如果为true,则直接按先进先出的方式,首位直接被唤醒

加锁源码分析

func (m *Mutex) Lock() {
	// 先判断是否已经被加锁,并且是无等待者,是的话,直接设置为加锁即可
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m)) // 这个在test 模式下非常用于,用于查看routine之间竞争
		}
		return
	}
	m.lockSlow()
}
func (m *Mutex) lockSlow() {
	var waitStartTime int64
	starving := false
	awoke := false
	iter := 0
	old := m.state // 拷贝之前的状态
	for {
		// old&(mutexLocked|mutexStarving) == mutexLocked : 如果不处于饥饿模式,并且已经被其他routine所锁定
		// runtime_canSpin(iter) 并且当前次数 未达到自旋次数临界值,则 自旋
		// 这里会有问题: 为什么处于被其他goroutine锁定时也要进行自旋? 原因在于: 正常模式下,会试图抢占锁
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			// !awoke: 表明当前goroutine 未唤醒
			// old&mutexWoken == 0: 当前lock中 没有goroutine 在尝试获取锁
			// old>>mutexWaiterShift 并且 没有goroutine 被阻塞  
			// 最后: 将自己设置为唤醒状态,并且将锁的状态也修改为唤醒
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			// 开始自旋
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
		// 跳出上述的for 循环表明:
		// old&(mutexLocked|mutexStarving) == mutexLocked: 不满足: 既,处于饥饿模式下,或者是 锁已经被人释放了
		// runtime_canSpin(iter): 达到自旋次数最大值
		
		new := old
		// 如果处于饥饿模式下, 不抢占,直接表明为locked (然后把自己丢到排队去)
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		// 处于饥饿模式,或者是被抢占了,则直接排队处理
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}

		// 如果在饥饿模式下被抢占了,则更新锁为饥饿锁,也就是说,如果不是饥饿模式或者锁没有被抢占则不会设置为饥饿模式
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		// 如果该goroutine被唤醒,要么是获得了锁,要么是处于休眠状态,总之不能是woken状态
		if awoke {
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			// 清除状态
			new &^= mutexWoken
		}
		// cas 交换更新锁状态: 可能只是更新为饥饿模式,不一定代表就是获取得到了锁
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			 // 如果锁之前是未锁定,且不处于饥饿状态,则表明,我们获得了锁,直接退出
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
		  
		  // 如果该goroutine为新的goroutine,则queueLifo 为false
		  // 否则为唤醒的goroutine,为 true
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			// 该函数的作用为: 如果是新来的goroutine,放到队尾,否则是唤醒的goroutine,放到队头
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			// 判断该goroutine 是否处于饥饿模式 (通过上述的starving 以及时间间隔)
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			// 如果该锁处于饥饿模式
			if old&mutexStarving != 0 {
				// 那么根据golang锁的定义,如果是饥饿模式下的锁,获取锁的必须是对头(既本goroutine),
				// 也就是说,当该goroutine被唤醒之后,锁不可以被别人给抢占,也不会有其他goroutine被唤醒去抢占
				// 并且因为是饥饿模式,所以当前goroutine肯定是在等待队列中的,因此 old>>mutexWaiterShift 不能为0
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				// 锁等待-1
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				// 如果当前goroutine是队列中最后一个,则退出饥饿模式
				if !starving || old>>mutexWaiterShift == 1 {
					delta -= mutexStarving
				}
				// 更新state,因为在上面我们已经 - 去了一些状态(如饥饿模式,等待数等),所以直 add即可
				atomic.AddInt32(&m.state, delta)
				break
			}
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}
解锁源码分析
  • 注意点
    • lock 可以被其他goroutine unlock
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// 先尝试直接解锁,如果为0,表明解锁成功
	
	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)
	}
}
func (m *Mutex) unlockSlow(new int32) {
// 解锁的前提是加锁,如果加锁没有,则是一个错误的状态,直接抛异常
	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
			}
			// Grab the right to wake someone.
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
			// 调用 sema.go  唤醒一个goroutine 
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		// 在饥饿模式下,handoff为true,直接唤醒的是第一个goroutine 
		// 参数handoff true 代表着直接唤醒等待队列goroutine中的第一个, 被唤醒的第一个,基于饥饿模式下,会直接获得锁
		// 而新来的goroutine会进到队尾
		runtime_Semrelease(&m.sema, true, 1)
	}
}


读写锁的实现

  • What:
    • 并发读之间不互斥
    • 并发写之间互斥
    • 并发读和写互斥
数据结构
type RWMutex struct {
	w           Mutex  // 既写锁,用于复用
	writerSem   uint32 // 写等待读的数
	readerSem   uint32 // 读等待写的数
	readerCount int32  // 当前正在读的goroutine数
	readerWait  int32  // 当写操作时,读等待的数
}
加读锁
func (rw *RWMutex) RLock() {
	// debug 模式下启用有很大好处
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// readerCount+1 ,如果readerCount<0 则认为是被写锁占用着,此时goroutine 等待阻塞被唤醒
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}
解读锁
func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	
	// 如果 >=0 ,说明解锁成功
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		// Outlined slow-path to allow the fast-path to be inlined
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}

func (rw *RWMutex) rUnlockSlow(r int32) {
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		throw("sync: RUnlock of unlocked RWMutex")
	}
	// 如果有写操作在等待,则释放信号量,让写操作尝试竞争获取锁
	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}
加写锁
func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 可能有多个routine在加锁,则复用之前的锁机制代码
	rw.w.Lock()
	// Announce to readers there is a pending writer.
	// 将readerCount 剪到最小(所以在加读锁或者解读锁的时候可以通过这个值来判断是否有写routine)
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders	
	// 将读操作等待的routine数追加到 写操作等待读的数 ,不为0 ,所有当前有读锁,则等待信号量的获取
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}
解写锁
func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// 先还原当加锁之前有多少个读routine在等待读取
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		race.Enable()
		throw("sync: Unlock of unlocked RWMutex")
	}
	// 因为写锁被占用之后,所有的读routine都被阻塞,所以要挨个唤醒
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// 可能此时还有其他的写锁routine 在尝试获取锁,则 转交给mutex操作
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

总结

  • 读写锁

    • 读锁:
      • 加读锁的时候,会先判断写操作是否已经占用,是的话,则会等待信号量获取
      • 解读锁的时候,同样的也是基于readCount是否<0 ,小于0则会去尝试唤醒写routine
    • 写锁:
      • 加写锁的时候,会先交给写锁routine之间竞争获取,然后再判断是否有读routine,有的话则阻塞,否则的话,设置状态表明有写routine了
      • 解写锁的时候,会先唤醒所有因为该写锁而阻塞的读routine,然后再交给mutex 释放写锁(因为期间可能还有其他写锁在)
  • golang的锁有2种模式

    • 正常模式和饥饿模式
    • 正常模式,gorputine之间会竞争获取锁,饥饿模式下,排队的形式获取锁
    • 当饥饿模式下,如果刚好最后一个goroutine获得锁,或者是获取锁的时间间隔在1ms内,则会恢复为正常模式
    • golang的锁充分运用位运算,低3位代表了 是否被锁定,是否有routine唤醒,以及锁的状态,剩下的位数则是routine的等待数
    • goroutine的唤醒与阻塞都是基于golang 的信号量机制实现

问题

  • 读写锁:

    • 写锁饥饿怎么处理,既 n个读routine,1个写routine
      • A: golang的读解锁的时候,会尝试唤醒写锁的routine,如果有写等待的话
    • 为什么读加锁,如果readerCount<0 就被阻塞
      • A: 因为 golang 的读写锁加了写锁之后,会将readCount 会赋值一个 增量的负值来表明 有写routine了,而读是需要等待写的(当然写也要等待读)
    • 为什么读解锁,如果readerCount>=0 就认为解锁成功
      • a. golang的读写锁是可以被其他routine给解开的
      • b. readerCount<0的情况为,只有当写锁抢占的时候会<0 ,所以是要阻塞等待的
    • 如何防止 读解锁之前是没有被RLock的
      • 是通过 内部的readerCount 判断的,readerCount 是当前正在读的goroutine数,没RLock表明为0 或者是被写锁 -maxRead ,
  • 什么是PAUSE指令

    • 使得进程(线程)挂起,直到收到信号,且信号函数返回成功,pause函数才会返回
  • 解锁问题

    • 为什么直接-mutexLocked 然后判断结果==0 就可以判断是否解锁成功,不为0的情况有哪些

    • 为什么饥饿模式下,唤醒的时候,不需要判断是否

  • 从正常模式到饥饿模式的触发条件

    • starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
    • **既当自旋时间超过了starvationThresholdNs 就会进入饥饿模式, starvationThresholdNs 为1e6 纳秒,既1ms **
  • 从饥饿模式恢复到正常模式的条件

  • 如果该goroutine获取得到了锁,并且满足下面任意一个条件都会转为正常竞争模式

    • 该goroutine为队列中最后一个
    • 等待的时间小于1ms
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值