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

相关推荐
IvorySQL20 分钟前
从轮询到等待——PostgreSQL WAIT FOR LSN 特性深度解析
数据库·postgresql
jimy132 分钟前
c++的 Type Traits (一)
开发语言·c++
OceanBase数据库官方博客1 小时前
被 VLDB 2027接收OceanBase 最新论文让数据库基(技术解析与实践)
数据库·人工智能·oceanbase
Web极客码1 小时前
Python Ollama 本地 Agent 入门与订单查询实践
开发语言·jvm·python
创邻科技2 小时前
国产图数据库刷新 LDBC 世界纪录:创邻科技 Galaxybase 在 SF100、SF300、SF1000 测试中全面领先”
数据库·科技
kimnoic2 小时前
PHP 8.2的DNF类型怎么用才规范
开发语言·php
GreatSQL社区2 小时前
主从复制SQL线程中断——字符集砸出的坑
java·数据库·算法
_Evan_Yao2 小时前
前后端联调怎么排查问题?从 Network 到后端日志,一步步定位错误
开发语言·后端·团队开发·需求分析
hudd6682 小时前
JavaEE 初阶 - 多线程(进阶)
java·开发语言·java-ee
XLYcmy2 小时前
AI 赋能后中文与英文数据库对比分析报告 上
数据库·ai·llm·prompt·检索·wos·cnki