golang读写锁

1、介绍

在sync.Mutex基础上实现的读写锁,支持读读并发,其他还是串行,适用于读多写少的场景

2、结构体

复制代码
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 atomic.Int32 // number of pending readers
    readerWait  atomic.Int32 // number of departing readers
}
  • w :一个Mutex,仅用于解决多个writer之间的竞争问题,

  • writerSem:一个信号量,用于阻塞writer等待正在进行的reader完成。就是用于写操作加锁时候要等待所有的读操作完成,当所有读操作解锁结束后,会发送信号,然后写操作被唤醒

  • readerSem:一个信号量,用于阻塞reader等待正在进行的writer完成。读操作要获取锁时候,需要等待写操作解锁,解锁后向该信号量发送信号,解锁所有读操作

  • readerCount:记录当前正在进行的reader的数量,也用于表示是否有writer正在等待。

  • readerWait :记录writer请求锁时需要等待完成的reader的数量。待完成的读操作计数器,仅有写操作等待时候有用,代表在写操作前已经在执行的读操作数量

核心流程

3.1 读加锁

Go 复制代码
func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	if rw.readerCount.Add(1) < 0 {
		// A writer is pending, wait for it.
		runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

原子操作 readCount 直接加1,如果结果不小于0,则说明加锁成功

如果小于0,则说明当前有writer正在等待或已经持有锁,则当前goroutine会阻塞在readerSem上,直到没有writer持有锁

注意: readCount小于0的原因是有写操作加锁时候会readCount-2的30次方,导致其值小于0

3.2 读解锁

复制代码
func (rw *RWMutex) RUnlock() {
    if race.Enabled {
       _ = rw.w.state
       race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
       race.Disable()
    }
    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) {
    if r+1 == 0 || r+1 == -rwmutexMaxReaders {
       race.Enable()
       fatal("sync: RUnlock of unlocked RWMutex")
    }
    // A writer is pending.
    if rw.readerWait.Add(-1) == 0 {
       // The last reader unblocks the writer.
       runtime_Semrelease(&rw.writerSem, false, 1)
    }
}

原子操作 readCount 直接减1,如果结果不小于0,说明无写协程等待,直接解锁完成

如果小于0,说明有写协程正在等待,进入慢流程:

原子操作把readerWait减1,若等于0 说明所有读操作都执行结束了,向writerSem发送信号,去唤醒等待的写协程。

大于0,说明还有其他读操作,则不发送信号量直接返回,解锁结束

3.3 写加锁

保证写操作互斥,等待所有的读操作结束

复制代码
func (rw *RWMutex) Lock() {
    if race.Enabled {
       _ = rw.w.state
       race.Disable()
    }
    // First, resolve competition with other writers.
    rw.w.Lock()
    // Announce to readers there is a pending writer.
    r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
    // Wait for active readers.
    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))
    }
}

先获取mutex的lock锁,确保同时只有一个写操作能进入后续

获取到互斥锁后,将readerCount设置为一个负数(通常是-readerCount-1),表示有writer正在等待锁。如果有正在进行的reader,writer会阻塞在writerSem上,直到所有reader都释放了锁。

复制代码
r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders 

精妙之处在于,先把readCount变为负数(使得后面的再进来的读操作进入阻塞),然后又加回来赋值给临时变量r,走到后面判断,如果有读操作还在运行,把readWait设置为原readCount的值,如果大于0,则写操作进入阻塞太,等待所有读操作完成

if r != 0
  • 如果 r == 0,说明没有活跃读者,writer 可以直接获得锁,无需等待。
  • 如果 r > 0,说明有 r 个读者正在读,需要等他们退出。
b. rw.readerWait.Add(r) (如果有读操作在运行,则肯定大于0)
  • readerWait 设置为 r(因为初始为 0,Add(r) 后就是 r);
  • readerWait 表示:"我这个 writer 需要等 r 个读者退出"。

⚠️ 注意:这里 Add(r) 的返回值是更新后的值 。由于初始为 0,返回值就是 r,所以 != 0 恒成立(只要 r != 0)。

c. runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
  • 调用底层 runtime 函数,在 writerSem 信号量上阻塞当前 goroutine
  • 会一直 sleep,直到有读者在 RUnlock() 时发现 readerWait == 0,然后 runtime_Semrelease 唤醒它。

✅ 此时,writer 被挂起,等待所有"已进入"的读者退出。

3.4 写解锁

复制代码
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 := rw.readerCount.Add(rwmutexMaxReaders)
    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()
    }
}

释放写锁。将readerCount恢复为正数(通过加上一个常数,通常是rwmutexMaxReaders 2的30次方),表示writer已经释放了锁,此时如果有等待的reader或writer,它们可以根据情况被唤醒。

如果r大于0,代表有多个r还在等待,则通过runtime_Semrelease(&rw.readerSem, false, 0) 发送解锁信号量,唤醒读操作

等于0 则说明 没有读操作在等下,继续往下,

执行mutex.Unlock互斥锁解锁,写操作解锁完成,解锁过程中通知其他写操作被唤醒

相关推荐
gopyer3 小时前
180课时吃透Go语言游戏后端开发11:Go语言中的并发编程
golang·go·游戏开发·并发编程
重生之我是Java开发战士3 小时前
【MySQL】数据库基础
数据库·mysql
2301_789015623 小时前
算法与数据结构——排序算法大全
c语言·开发语言·数据结构·c++·算法·排序算法·visual studio
ChuHsiang3 小时前
【剑指MySQL】数据库基础(1)
数据库·mysql
学习编程的Kitty3 小时前
JavaEE初阶——多线程(1)初识线程与创建线程
java·开发语言·java-ee
muxin-始终如一3 小时前
MySQL分区分表实现方法详解
数据库·mysql·adb
勤奋菲菲3 小时前
Egg.js 完全指南:企业级 Node.js 应用框架
开发语言·javascript·node.js
Tomorrow'sThinker3 小时前
第三章 · 数据库管理与视频路径获取
数据库·oracle
IndulgeCui4 小时前
【金仓数据库产品体验官】Mycat适配KES分库分表体验
数据库