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互斥锁解锁,写操作解锁完成,解锁过程中通知其他写操作被唤醒

相关推荐
YOU OU3 小时前
Redis基础常识与命令
数据库·redis·缓存
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海4 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
Database_Cool_5 小时前
OLTP 和 OLAP 区别详解:分析型数据库和事务型数据库怎么选(附阿里云 AnalyticDB MySQL 选型指南)
数据库·mysql·阿里云
爱写代码的阿森6 小时前
鸿蒙三方库 | harmony-utils之KvUtil键值型数据库操作详解
数据库·华为·harmonyos·鸿蒙·huawei
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
Database_Cool_6 小时前
单机 MySQL 迁移到分布式数据库方便吗?阿里云 PolarDB-X 100% MySQL 协议兼容零改造平滑迁移
数据库·分布式·mysql