Go的sync包下Mutex和RWMutex源码个人解读

1.1 Mutex核心机制

1.1.1 上锁和解锁操作

简单一点的理解锁就是一个状态值,比如0表示未加锁,1表示已加锁。上锁操作就是把状态值从0改成1,解锁操作就是把状态值从1改成0。如果修改失败就表明锁的掌控权不在你手上。

1.1.2 Mutex的升级过程 自旋 -> 阻塞

一个优秀的工具需要具备探测并适应环境,从而采取不同的对策因地制宜的能力。

现在有两种策略:

  • 自旋+CAS:基于自旋结合CAS的方式,重复校验锁的状态并尝试获取锁,始终把主动权掌握在自己手上

优势:无需阻塞协程,短期来看操作较轻

劣势:长时间争而不得会浪费CPU时间片

适用场景:并发竞争强度低的场景

  • 阻塞/唤醒:将当前goroutine阻塞唤起,直到锁被释放后,以回调的方式将阻塞goroutine重新唤醒,进行锁争夺

优势:精准打击,不浪费CPU时间片

劣势:需要挂起协程,进行上下文切换,操作较重

适用场景:并发竞争激烈场景

sync.Mutex结合两种方案的使用场景,制定了一个锁升级的过程,反映了面对并发环境通过持续试探逐渐转化为悲观的态度,具体方案如下:

  • 首先保持乐观,goroutine采用自旋+CAS的策略争夺锁
  • 尝试持续受挫达到一定条件后,判定当前竞争条件过于激烈,则由自选转为阻塞/挂起模式

上面谈及的自选转为阻塞/挂起模式具体条件拆解如下:

  • 自旋累计达到4次仍未取得战果
  • CPU单核或仅有单个P调度器(此时自旋,其他goroutine根本没机会释放锁,自旋纯属空转)
  • 当前P的执行队列中仍然有待执行的G(避免因自旋影响到GMP调度效率)

1.1.3 饥饿模式

上面的1.1.2的升级策略主要面向性能问题,这里引入的饥饿模式概念主要是为了探讨公平性问题

概念:

  • 饥饿:顾名思义,是因为非公平机制的原因,导致Mutex阻塞队列中存在goroutine长时间去不到锁,从而陷入饥荒状态
  • 饥饿模式:当Mutex阻塞队列中存在处于饥饿状态的goroutine时,满足一定条件,会进入该模式,将抢锁流程由非公平机制转为公平机制

在sync.Mutex运行过程中存在两种模式:

  • 正常模式/非饥饿模式:这是sync.Mutex默认采用的模式。当有goroutine从阻塞队列被唤醒时,会和此时先进入抢锁流程的goroutine进行锁资源的争夺,假如抢锁失败,会重新回到阻塞队列头部

*:此时被唤醒的老goroutine相比新goroutine是处于劣势地位,因为新goroutine已经在占用PCPU时间片,且新goroutine可能存在多个,从而形成多对一的人数优势,因此形势对老goroutine不利

  • 饥饿模式:这是sync.Mutex为拯救陷入饥荒的老goroutine而启用的特殊机制,饥饿模式下,锁的所有权按照阻塞队列的顺序进行依次传递,新goroutine进行流程时不得抢锁,老老实实进入队列尾部排队

两种模式的转换条件:

  • 默认为正常模式
  • 正常模式 -> 饥饿模式:当阻塞队列存在goroutine等锁超过1ms而不得,被唤醒时再次抢锁失败就会开启饥饿模式
  • 饥饿模式 -> 正常模式:当阻塞队列已清空,或取得锁的goroutine等锁时间已经低于1ms,则回到正常模式

小结:正常模式灵活机动,性能较好;饥饿模式严格死板坚决捍卫公平底线。因此两种模式的切换体现了sync.Mutex为适应环境变化,在公平和性能之间做出的调整和权衡。

个人看法:这个正常模式 -> 饥饿模式这个过程不是说有goroutine被唤醒时检测等锁超过1ms就一定会开启饥饿模式的,此时只是会标识局部变量staving=true,表示在下一次被唤醒的goroutine如果还没有抢锁成功的话才会真正的开启饥饿模式,否则被唤醒的goroutine抢锁成功的话是不会开启饥饿模式的,这样就会导致极端条件下,阻塞队列的goroutine还是不能得到公平性的保证。

1.1.4 goroutine唤醒标识

为了尽可能缓解竞争压力和性能损耗,sync.Mutex会不遗余力在可控范围内减少一些无意义的并发竞争和操作损耗。

在实现,sync.Mutex通过一个MutexWoken标识位,标志出当前是否已有goroutine在自旋抢锁或存在goroutine从阻塞队列中唤醒;倘若mutexWoken为true,且此时有解锁动作发生时,就没必要再额外唤醒阻塞的goroutine从而引起竞争内耗。

1.2 数据结构

type Mutex struct {
	state int32
	sema  uint32
}
  • state:锁中最核心的状态字段,不同bit位分别存储了mutexLocked(是否上锁)、mutexWoken(是否有goroutine从阻塞队列中被唤醒)、mutexStaving(是否处于饥饿模式)的信息
  • sema:用于阻塞和唤醒goroutine的信号量

1.2.1 几个全局变量

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

	// Mutex fairness.
	//
	// 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.
	starvationThresholdNs = 1e6
)
  • mutexLocked = 1:state最右侧的一个bit位标志是否上锁,0-未上锁,1-已上锁
  • mutexWoken = 2:state右数第二个bit位标志是否有goroutine从阻塞中被唤醒,0-没有,1-有
  • mutexStaving = 4:state右数第三个bit位标志Mutex是否处于饥饿模式,0-非饥饿模式,1-饥饿模式
  • mutexWaiterShift = 3:右侧存在3bit位标识特殊信息,分别为上述的mutextLocked、mutexWoken、mutexStaving;主要是为了右移取得当前阻塞队列的goroutine个数
  • stavationThresholdNs = 1ms:sync.Mutex进入饥饿模式的等待时间阈值

1.2.2 state字段详述

Mutex.state 字段位int32类型,不同bit位具有不同的表示含义:

(借用小徐先生的图解)

低3位分别标识mutexLocked(是否上锁)、mutexWoken(是否有协程在抢锁)、mutexStaving(是否有协程在抢锁),高29位的值聚合为一个范围的0-2^29-1的整数,表示在阻塞队列中等待的协程个数

在源码中使用了位运算从Mutex.state字段获取上述信息:

  • state & mutexLocked: 判断是否上锁
  • state | mutexLocked:加锁
  • state & mutexWoken:判断是否有协程正在抢锁
  • state | mutexWoken:更新状态,表示存在抢锁的协程
  • state &^ mutexWoken:更新状态,标识不存在抢锁的协程(x &^ y ,例如y=1,结果为0,假若y=0,结果为x)
  • state & mutexStaving:判断是否处于饥饿状态
  • state | mutexStaving:置为饥饿模式
  • state >> mutexWaiterShif:获取阻塞等待的协程数
  • state += 1 << mutexWaiterShif:阻塞等待的携程数 + 1

1.3.1 源码 Mutex.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()
}
  • 首先先进行一轮CAS操作,如果当前处于正常模式、没有协程竞争锁并且没有上锁也没有阻塞的协程(也就是m.state = 0就直接加锁成功),则直接CAS抢锁成功返回
  • 第一轮初探失败,则进入lockSlow流程,下面细谈

1.3.2.1 Mutex.lockSlow()

几个局部变量

func (m *Mutex) lockSlow() {
	var waitStartTime int64
	starving := false
	awoke := false
	iter := 0
	old := m.state
    ...
}
  • waitStartTime:标识当前goroutine在抢锁过程中等待的时长,单位:ns
  • starving:标识当前协程是否饥饿
  • awoke:true标识当前协程正在进行抢锁操作 false标识当前协程已经退出抢锁操作
  • iter:标识当前goroutine参与自旋的次数
  • old:临时存储锁的state值

1.3.2.2 自旋空转

func (m *Mutex) lockSlow() {
    ...
    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分支
		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.
            // 因为此时该协程已经在自旋尝试上锁操作,需要更改全局变量m.state.mutexWoken = 1,
            //标识当前已经有协程正在尝试上锁操作,别的协程在释放锁的时候不需要再从等待队列唤醒协程避免不必要的竞争
			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循环
  • 假如满足三个条件:1.正常模式 2.锁被占用 3.满足自旋条件 则进入自旋后处理环节
  • 在自旋处理中,如果当前阻塞队列有正在等待该锁的阻塞协程,则通过CAS操作将state的mutexWoken标识更新为1(防止在释放锁的时候再从阻塞队列唤醒协程,因为此时已经有协程正在进行尝试上锁操作,没有必要增加竞争压力),将局部变量awoke更新为true
  • 调用runtime_doSpin告知调度器P当前处于自旋模式
  • 更新自旋次数iter和锁状态值old
  • 通过continue进入下一轮的尝试

1.3.2.3 state数值更新

func (m *Mutex) lockSlow() {
    // ...
    for {
        ... 
        //到此说明有三种情况 1.自旋抢锁失败 2.处于饥饿模式下锁的拥有状况不确定 3.锁已经被别的协程释放
        // new值用于更新state
        new := old
		// Don't try to acquire starving mutex, new arriving goroutines must queue.
        // 如果旧值是正常模式就加锁 *注意:这里的锁可能被别的协程拥有因为上面的情况1和2
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
        // 此时如果旧值仍然被别的协程上锁或者处于饥饿模式下就需要进入阻塞队列里等待,因为此协程用完了两次机会都没能够上锁成功(第一次是首次调用方法时尝试atomic.CompareAndSwapInt32(&m.state, 0, 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.
        // 如果此时的staving为true并且锁被别的协程上锁,则需要开启饥饿模式确保此协程下次一定能够获得锁的拥有权  *staving的状态更改在下面的操作
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
        // awoke=true表明此协程在抢锁的时候设置awoke=true,主要是为了防止别的协程释放锁的时候唤醒阻塞队列的协程跟此协程抢锁,经过上面的操作后,到此处无论此(我)协程是拿到锁的拥有权还是自旋尝试上锁失败都需要通知别的协程(我)已经不需要再抢锁了也就是更新mutexWoken = 0
		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
		}

        ... 
        
    }
}

  • 从自旋操作出来,到此说明有三种情况 1.自旋抢锁失败 2.处于饥饿模式下锁的拥有状况不确定 3.锁已经被别的协程释放 无论是什么情况,都会对sync.Mutex的状态值state进行更新
  • 如果旧值是正常模式就尝试进行加锁 *注意:这里的锁可能被别的协程拥有因为上面的情况1和2
  • 此时如果旧值仍然被别的协程上锁或者处于饥饿模式下(饥饿模式下能够进行加锁的操作是在协程被从阻塞队列唤醒时才进行,不可能在此进行)就需要进入阻塞队列里等待,因为此协程用完了两次机会都没能够上锁成功(第一次是首次进入方法时尝试CAS加锁,第二次是上面自旋尝试加锁失败。)则表明此协程这一轮注定无法抢锁成功,可以直接让新值的阻塞协程数+1
  • 倘若当前协程已经处于饥饿状态并且旧值还被别的协程上锁,则设置新值为饥饿模式,确保下次饥饿模式此协程能够获得锁
  • 如果局部变量awoke=true表示全局变量Mutex.state中mutexWoken被设置为1,有可能时此协程亲自设置的,也有可能是因为别的协程在正常模式下释放锁时帮此协程设置的,但是当前的协程要么抢锁成功,要么被阻塞挂起,所以新值需要设置mutexWoken = 0表示此协程协程已经退出抢锁操作

1.3.2.4 state新旧值替换

func (m *Mutex) lockSlow() {
    // ...
    for {
        // 自旋抢锁失败后处理 ...
        
        // new old 状态值更新 ...
        
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // case1 加锁成功
            // case2 将当前协程挂起
            
            // ...
        }else {
            old = m.state
        }
        // ...
    }
}
  • 通过CAS操作更新Mutex.state的值
  • 如果失败(即有其他协程介入提前更改了state导致不符合预期),则设置旧值为此刻的Mutex.state ,并开启新一轮的循环
  • 如果CAS替换成功,则进入最后一轮的二选一局面:case1:倘若当前goroutine加锁成功,则直接返回 case2:直接将goroutine挂起加入阻塞队列

1.3.2.5 case1 上锁成功情况

func (m *Mutex) lockSlow() {
    // ...
    for {
        // 自旋抢锁失败后处理 ...
        
        // new old 状态值更新 ...
        
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 因为上面设置新值的mutexLocked为1时有可能mutexLocked本身就是1,所以此处需要判断旧值有没有被别的协程上锁,没有被别的协程上锁,则表明当前协程已经上锁成功,直接返回
            if old&(mutexLocked|mutexStarving) == 0 {
                break 
            }
            
            // ...
        } 
        // ...
    }
}
  • 延续1.3.2.4的思路,此时已经将Mutex.state的旧值更换成新值
  • (注意1.3.2.3表明的注意情况)需要再次判断锁的值是否是此协程更改的,接下来进行判断,如果旧值是未加锁且为正常模式,就意味着加锁标识位正是由当前goroutine完成的更新,说明加锁成功,返回即可
  • 如果旧值中锁未释放或者处于饥饿模式,则当前goroutine需要进入阻塞队列挂起

1.3.2.6 case2 阻塞挂起

func (m *Mutex) lockSlow() {
    // ...
    for {
        // 自旋抢锁失败后处理 ...
        
        // new old 状态值更新 ...
        
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 加锁成功后返回的逻辑分支 ...
            
            // queueLifo标识当前协程是否是第一次进入阻塞队列 
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                // 获取当前的纳秒数
                waitStartTime = runtime_nanotime()
            }
            // 将协程添加到阻塞队列中
            runtime_SemacquireMutex(&m.sema, queueLifo, 1)
            // ...
        } 
        // ...
    }
}
  • 接着1.3.2.5,到了此处,只有两种情况,要么是抢锁失败,要么是处于饥饿状态,无论是什么情况,此协程注定要被阻塞挂起
  • 判断queueLifo 标识当前goroutine是从阻塞队列唤醒的老客还是第一个进入阻塞队列的新客
  • 如果此协程等待的起始时间为0,表示此协程是新客,会被放入阻塞队列尾部,如果不是0,则表示此协程是老客,会被放入阻塞队列头部
  • 挂起协程

1.3.2.7 从阻塞队列被唤醒

func (m *Mutex) lockSlow() {
    // ...
    for {
        // 自旋抢锁失败后处理...
        
        // new old 状态值更新 ...
        
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 加锁成功后返回的逻辑分支 ...
             
            // 挂起前处理 ...
            runtime_SemacquireMutex(&m.sema, queueLifo, 1)
            // 从阻塞队列被唤醒了,判断等待的时间是否达到开启饥饿模式的阈值
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            // 重新获取Mutex.state的值
            old = m.state
            // 如果是饥饿模式表示此被唤醒的协程一定加锁成功
            if old&mutexStarving != 0 {
                // 更改Mutex.state的值 加锁并且阻塞队列数量-1
                delta := int32(mutexLocked - 1<<mutexWaiterShift)
                if !starving || old>>mutexWaiterShift == 1 {
                    // 如果当前协程不处于饥饿状态或者等待队列就只剩此协程一个就没有必要再开启饥饿模式
                    delta -= mutexStarving
                }
                // 更新状态
                atomic.AddInt32(&m.state, delta)
                break
            }
            // 正常模式下被唤醒的协程接着自旋
            awoke = true
            iter = 0
        } 
        // ...
    }
}
  • 到此处,说明当前goroutine是从Mutex的阻塞队列中被唤醒的
  • 判断一下是否需要当前协程是否处于饥饿状态,如果当前goroutine进入阻塞队列的时间超过1ms,则说明该协程已经处于饥饿状态,在下一次循环中如果还没抢到锁才会开启饥饿模式。
  • 获取此时锁的状态,通过old存储
  • 如果是饥饿模式,则表明该协程一定加锁成功,更新锁的状态并且将阻塞队列数量-1,接着判断是否需要关闭饥饿模式,最终通过原子操作添加到Mutex.state中

1.4 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.
    // 快速通道 Mutex.State直接减1
	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.
        // 如果是正常模式并且阻塞队列没有协程,那么new必然在上面操作后变为0,不为零说明要么异常要么有阻塞协程要么为饥饿模式
		m.unlockSlow(new)
	}
}
  • 通过原子操作解锁
  • 如果解锁时发现目前是正常模式并且只有自身一个goroutine,则直接返回成功即可
  • 如果new != 0则表示可能是释放锁异常、饥饿模式、阻塞队列还有协程,然后进入unlockSlow()

1.4.2 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.
            // 更新新值,设置等待的协程数-1和标识有协程正在抢锁
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
                // 更新新值,从等待队列头部唤醒协程
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
            // 如果修改失败则表示有别的协程修改了Mutex.state的值,所以重新获取state的值重新操作
			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.
        // 饥饿模式直接从等待队列头部释放协程,让被唤醒的协程自己更新Mutex.state的值
		runtime_Semrelease(&m.sema, true, 1)
	}
}
  • 解锁时如果发现之前Mutex并未加锁,直接抛出fatal
  • 正常模式下,如果阻塞队列无goroutine或者mutexLocked、mutexStaving、mutexWoken标识位不为零的话,说明此时已经有别的活跃协程介入,自身无需关心后续流程
  • 基于CAS操作将Mutex.state中阻塞协程数减1,如果成功,则唤起阻塞队列头部的goroutine并退出
  • 如果减少阻塞协程数的CAS失败,则更新此时的Mutex.state为新的old值,开启下一轮循环
  • 饥饿模式下直接唤醒阻塞队列头部的goroutine即可

2.1 Sync.RWMutex核心机制

  • 逻辑上可以把RWMutex理解为一把读锁加一把写锁
  • 写锁具有严格的排他性,当其被占用,其他试图取写锁或者读锁的goroutine均阻塞
  • 读锁具有有限的共享性,当其被占用,试图取写锁的goroutine会阻塞,试图取读锁的goroutine可与当前goroutine共享读锁
  • 综上所述,RWMutex适用于读多写少的场景,最理想化的情况,当所有操作均使用读锁,则可实现去无化;最悲观的情况,倘若所有操作均使用写锁,则RWMutex退化成普通的Mutex

2.2 Sync.RWMutex数据结构

type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // semaphore for writers to wait for completing readers
	readerSem   uint32 // semaphore for readers to wait for completing writers
	readerCount int32  // number of pending readers
	readerWait  int32  // number of departing readers
}


const rwmutexMaxReaders = 1 << 30
  • rwmutexMaxReaders:共享读锁的goroutine数量上限,值为2^29
  • w:RWMutex内置的一把普通互斥锁sync.Mutex
  • writerSem:关联写锁阻塞队列的信号量
  • readerSem:关联读锁阻塞队列的信号量
  • readerCount:正常情况下等于介入读锁流程的goroutine数量;当goroutine介入写锁流程时,该值为实际介入读锁流程的goroutine数量减 rwmutexMaxReaders(正数表示当前介入读流程的goroutine数量,负数表示有写锁介入)
  • readerWait:记录当前goroutine获取写锁前,还需要等待多少个goroutine释放读锁

2.3.1 读锁流程 RLock

func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
    // 直接进行原子操作对readerCount+1,如果是正数则添加读锁成功,如果为负数,表示在此协程进行加读锁之前添加了写锁,所以直接进入阻塞队列
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		// A writer is pending, wait for it.
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}
  • 基于原子操作,将RWMutex的readCount变量加一,表示占用或等待读锁的goroutine数量加一
  • 如果RWMutex.readerCount的新值仍然小于0,说明goroutine为释放写锁,因此将当前goroutine添加到读锁的阻塞队列中并阻塞挂起

2.3.2.1 RUnlock

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
    // 基于原子操作直接让readerCount - 1,如果大于等于零,则表示释放锁成功,如果小于零,则可能之前并未加读锁或者之前获取读锁的协程被阻塞了,不然锁的主动权不会到此释放读锁的协程上,所以得去唤醒被阻塞的协程
	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()
	}
}
  • 基于原子操作,将RWMutex的readCount变量减一,表示占用或等待读锁的goroutine数减一
  • 如果RWMutex.readCount的新值小于0,说明有协程正在等待获取写锁,进入UnlockSlow()

2.3.2.2 rUnlockSlow()

func (rw *RWMutex) rUnlockSlow(r int32) {
    // 1.如果之前没有上读锁或者读锁的数量到达最大共享读锁的数量 r+1 == 0
    // 2.r+1 == rwmutexMaxReaders 因为r的值为负,说明有goroutine介入写操作,但是如果这种条件成立则说明之前并未加过读锁 
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		fatal("sync: RUnlock of unlocked RWMutex")
	}
	// A writer is pending.
    // 判断此goroutine是否是介入读锁流程的最后一个协程,最后一个需要唤醒一个等待写锁的阻塞队列的goroutine
	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}
  •     // 1.如果之前没有上读锁或者读锁的数量到达最大共享读锁的数量 r+1 == 0
        // 2.r+1 == rwmutexMaxReaders 因为r的值为负,说明有goroutine介入写操作,但是如果这种条件成立则说明之前并未加过读锁 
  • 首先进入此方法的条件是读流程后面有正在等待写锁的goroutine,介入读流程并且最靠近的写流程的那一个读goroutine释放锁时需要唤醒正在等待写锁的goroutine
  • 基于原子操作对readerWait减一,如果readWait = 0,表示当前goroutine就是最后一个介入读流程的协程,因此需要唤醒一个等待写锁的阻塞队列的goroutine(因为goroutine1尝试进行加写锁时,如果前面有goroutine2进行了读操作,那么goroutine1就会进入阻塞队列挂起等待)

2.4.1 写锁流程 Lock

// Lock locks rw for writing.
// If the lock is already locked for reading or writing,
// Lock blocks until the lock is available.
func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// First, resolve competition with other writers.
    // 调用sync.Mutex.Lock() 尝试获取一把互斥锁
	rw.w.Lock()
	// Announce to readers there is a pending writer.
    // 判断在此协程之前有没有goroutine加了读锁并且更新readerCount的值
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// Wait for active readers.
    // 如果r==0,则表示之前没有加读锁,此协程加写锁成功。
    // 如果r != 0 表示在此写成之前已经有goroutine加了读锁,更新一下在此goroutine之前还需要等待多少个goroutine释放读锁 然后阻塞等待此goroutine之前的加最后以把读锁的goroutine释放读锁唤醒此goroutine
	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))
	}
}
  • 对RWmutex内置的互斥锁进行加锁操作
  • 基于原子操作,对RWMutex.readerCount进行减rwmutexMaxReaders的操作
  • 如果此时存在还未释放读锁的goroutine,则基于原子操作在RWMutex.readerWait的基础上加上介入读流程的goroutine数量,并将当前goroutine添加到写锁的阻塞队列中挂起

2.4.2 写锁解锁 Unlock

func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// Announce to readers there is no active writer.
    // 基于原子操作让RWMutex.readerCount - rwmutexMaxReaders
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    // 如果 r >= rwmutexMaxReaders 则说明在此读锁之前还有goroutine正在使用读锁,因此还不能释放写锁直接fatal
	if r >= rwmutexMaxReaders {
		race.Enable()
		fatal("sync: Unlock of unlocked RWMutex")
	}
	// Unblock blocked readers, if any.
    // 释放写锁之前会先唤醒正在等待此协程释放写锁的协程
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// Allow other writers to proceed.
    // 释放写锁
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}
  • 基于原子操作,将RWMutex.readerCount的值加上rwmutexMaxReaders
  • 如果readerCount的新值大于等于rwmutexMaxReaders,则说明要么当前RWMutex未上过写锁,要么就是在此goroutine之前还有没释放读锁的goroutine,所以不可以释放直接fatal
  • 先唤醒读锁阻塞队列中的所有goroutine(可见,竞争读锁的goroutine更具备优势)
  • 解开RWMutex内置的互斥锁

个人看法总结:

        1.sync.Mutex进行上锁的过程判断复杂一些,先进行一次CAS操作尝试上锁,失败后先尝试自旋加锁,然后更新Mutex.state的值,然后如果是正常模式下,判断加锁成功直接返回,否则就进入阻塞队列挂起,等待别的协程唤醒重置自选次数重新尝试自旋加锁。饥饿模式下,被唤醒直接获取锁返回
        2.sync.Mutex释放锁的过程主要的看有没有这个必要需要当前协程释放,如果调用Unlock()的协程更适合释放锁就让它释放,否则就让别的协程释放
        3.sync.Mutex的这个饥饿模式是蛮公平的,但是前提是你得开启饥饿模式,这个条件需要被唤醒的goroutine检测到等待锁的时间超过1ms,并且还需要在下一轮循环以正常模式去跟新来尝试抢锁的goroutine竞争锁失败的情况下才会开启,极端情况下,可能会出现阻塞队列的goroutine等待时间超过1ms,但是因为上一个从等待队列出来的饥饿的goroutine以正常模式抢到锁,这个饥饿模式就开不了(纯个人看法)
        4.sync.RWMutex加读锁就是直接原子操作readerCount+1,readCount是正数就表示加锁成功,负数就表示在此之前有读锁,然后进入阻塞队列等待读锁释放锁时唤醒
        5.sync.RWMutex释放读锁就是直接原子操作readerCount-1,只要不是负数就是放成功,是负数的话就表明有goroutine想要加写锁没成功进入了阻塞队列需要靠近写锁的最后一个读goroutine唤醒
        6.sync.RWMutex加写锁需要先加互斥锁也就是sync.Mutex的Lock,然后判断在它之前有没有goroutine加了读锁,没有就加写锁成功,有的话就更新一下它前面有多少个goroutine正在使用读锁,然后进入阻塞队列挂起等待唤醒
        7.sync.RWMutex释放写锁会先更新readerCount的值,然后先释放正在等待此读锁释放的goroutine,最后才释放sync.Mutex
        8.sync.RWMutex对写操作是不当人,就是读的优先级比较高,加个写锁需要进行两次竞争,一次是sync.Mutex.Lock(),另一次是要和sync.RWMutex的读锁竞争,而且,比较不当人的是,会有这种情况,明明goroutine1在等待加锁队列的前面,但是sync.RWMutex释放写锁的时候优先释放等待队列的想要进行读操作的goroutine2,如下,第三个writer大冤中就是很好的例子,爱不爱一眼就可见

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值