golang sync.RWMutex源码解析

sync.RWMutex

版本: go1.20.3 darwin/arm64

下面有四个场景

  • 写操作如何防止写操作?

直接加互斥锁

  • 写操作是如何阻止读操作的?

RWMutex.readerCount是个整型值,用于表示读者数量,不考虑写操作的情况下,每次读锁定将该值+1,每次解除读锁定将该值-1

进行写操作时候,会将RWMutex.readerCount变成负值

  • 读操作是如何阻止写操作的?

每次读的时候,会将RWMutex.readerCount减一,当RWMutex.readerCount变为0后,进行写操作

  • 写操作会不会饿死?

首先解释一下为什么可能有饿死的情况发生:写操作要等待读操作结束后才可以获得锁,写操作等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能被饿死。

当写操作来的时候,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,然后每次进行读操作RWMutex.readerCount RWMutex.readerWait都会减1,当readerwait=0时,开始写操作

解析

type RWMutex struct {
	w           Mutex  // 互斥锁
	writerSem   uint32 // 写操作等待读操作完成的信号量
	readerSem   uint32 // 读操作等待写操作完成的信号量
	readerCount atomic.Int32  // 读锁计数器
	readerWait  atomic.Int32  // 获取写锁时当前需要等待的读锁释放数量
}

// 最大只支持 1 << 30 个读锁
const rwmutexMaxReaders = 1 << 30

信号量(semaphore)

  • 获取(acquire,又称 wait、decrement 或者 P)
  • 释放(release,又称 signal、increment 或者 V)

获取操作把信号量减一,如果减一的结果是非负数,那么线程可以继续执行。如果结果是负数,那么线程将会被阻塞,除非有其它线程把信号量增加回非负数,该线程才有可能恢复运行)。

释放操作把信号量加一,如果当前有被阻塞的线程,那么它们其中一个会被唤醒,恢复执行。

Go 语言的运行时提供了 runtime_SemacquireMutexruntime_Semrelease 函数,像 sync.RWMutex 这些对象的实现会用到这两个函数。

写锁加锁 Lock()

func (rw *RWMutex) Lock() {
    // 竞态检测
    if race.Enabled {
        _ = rw.w.state
        race.Disable()
    }
    // 1.使用 Mutex 锁,解决与其他写者的竞争
    rw.w.Lock()
    
    // 2.判断当前是否存在读锁:先通过原子操作改变readerCount(readerCount-rwmutexMaxReaders),
    // 使其变为负数,告诉 RUnLock 当前存在写锁等待;
    // 然后再加回 rwmutexMaxReaders 并赋给r,若r仍然不为0, 代表当前还有读锁
    	r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
    
    // 3.如果仍然有其他 Goroutine 持有互斥锁的读锁(r != 0)
    // 会先将 readerCount 的值加到 readerWait中,防止源源不断的读者进来导致写锁饿死,
    // 然后该 Goroutine 会调用 sync.runtime_SemacquireMutex 进入休眠状态,
    // 并等待所有读锁所有者执行结束后释放 writerSem 信号量将当前协程唤醒。
  	if r != 0 && rw.readerWait.Add(r) != 0 {
		runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
	}
    // 竞态检测
    if race.Enabled {
        race.Enable()
        race.Acquire(unsafe.Pointer(&rw.readerSem))
        race.Acquire(unsafe.Pointer(&rw.writerSem))
    }
}

首先会加互斥锁,然后会检查是否有其他的读锁,检查方式就是readerCount原子操作-rwmutexMaxReaders,这个时候readerCount变负的阻止写,然后这个值再补回+rwmutexMaxReaders得到r,如果r!=0,说明此时有其他的读锁,那么为了防止写锁饿死,就将waitCount用原子操作+r,然后调用信号量休眠该goroutine

写锁释放 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.
  // r现在就是读锁的数量
	r := rw.readerCount.Add(rwmutexMaxReaders)
  // 若超过读锁的最大限制, 触发panic
	if r >= rwmutexMaxReaders {
		race.Enable()
		fatal("sync: Unlock of unlocked RWMutex")
	}
	// Unblock blocked readers, if any.
  // 逐个调用信号量唤醒goroutine
	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()
	}
}

没什么好讲的,直接看注释就可以了

就是给readerCount加上max值,然后循环采用信号量的方式唤醒coroutine,最后释放互斥锁

读锁加锁 RLock()

func (rw *RWMutex) RLock() {
  // 是否开启检测race
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
  //这里分两种情况:
	// 1.此时无写锁 (readerCount + 1) > 0,那么可以上读锁, 并且readerCount原子加1(读锁可重入[只要匹配了释放次数就行])
	// 2.此时有写锁 (readerCount + 1) < 0,所以通过readerSem读信号量, 使读操作睡眠等待
	if rw.readerCount.Add(1) < 0 {
		// A writer is pending, wait for it.
    // 当前有个写锁, 读操作需要阻塞等待写锁释放;
    // 其实做的事情是将 goroutine 排到G队列的后面,挂起 goroutine
		runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

直接readerCount+1,然后<0说明此时有写锁,调用信号量睡眠

读锁释放 RUnlock()

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
  // 写锁等待状态,检查当前是否可以进行获取;
    // 首先将 readerCount 减1并赋予r,然后分两种情况判断
    //  1.若r大于等于0,读锁直接解锁成功,直接结束本次操作;
    //  2.若r小于0,有一个正在执行的写操作,在这时会调用sync.RWMutex.rUnlockSlow 方法;
	if r := rw.readerCount.Add(-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) {
      // r + 1 == 0 表示本来就没读锁, 直接执行RUnlock()
    // r + 1 == -rwmutexMaxReaders 表示执行Lock()再执行RUnlock()
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		fatal("sync: RUnlock of unlocked RWMutex")
	}
	// A writer is pending.
  // 如果当前有写锁等待,则减少一个readerWait的数目
	if rw.readerWait.Add(-1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

先检查是否直接执行RUnlock()||执行Lock()再执行RUnlock(),然后给waitCount-1,与0比较,如果==0,就调用信号量唤醒goroutine

参考

https://dongxiem.github.io/2020/06/07/golang-sync-bao-yuan-ma-pou-xi-2-sync.rwmutex/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值