bigywater.tech
Go · 76

RWMutex:写者饥饿和读者递归加锁的行为

RWMutex 的文档里有一句不起眼的话:写者排队后,新的读者会阻塞。这意味着什么?这篇用实验复现递归读锁死锁,看写者排队的时间线,量写者会不会饿死,并测临界区多长读写锁才值得。

deadlock
持读锁时递归 RLock、且有写者排队,运行时报 all goroutines are asleep
0.60–1.01 ms
8 个读者不停读时,写者每次 Lock 的等待范围
8.1×
100 µs 临界区、8 个只读 goroutine,Mutex 对 RWMutex 的总耗时倍数

递归读锁是个坑

实测 · 本机执行输出
  B: 调用 Lock …
  A: 已持有读锁,现在第二次 RLock …
  fatal error: all goroutines are asleep - deadlock!

A 持有读锁、B 在 Lock 排队时,A 再 RLock 会被挡在写者后面,而 B 等着 A 的第一次读锁——典型死锁,运行时在所有 goroutine 都睡着时报 fatal error。文档明确说这禁止递归读锁。注意嵌套调用链里的“读锁函数调用读锁函数”。

写者排队后新读者要等

实测 · 本机执行输出
  R1 在 0 ms 拿到读锁并持有 100 ms;W 在 10 ms 请求写锁,在 101 ms 拿到;R2 在 20 ms 请求读锁,在 122 ms 才拿到
  已有读者、且有写者在排队时,TryRLock() = false
  只有读者、没有写者时(换新锁),TryRLock() = true

R1 持读锁 100 ms,W 在 10 ms 请求写锁,R2 在 20 ms 请求读锁:R2 直到 122 ms(W 拿到 101 ms 并持有 20 ms 之后)才拿到,尽管读锁本来可以并存。TryRLock 在这种情形下返回 false。

写者不会饿死

实测 · 本机执行输出
  8 个读者连续读(每次持锁约 1 ms),写者 30 次 Lock 的等待:最小 0.60 ms / 中位 0.91 ms / 最大 1.01 ms

读者连续读(每次约 1 ms)时,写者 30 次请求的等待是 0.60–1.01 ms,正好是当前读者的持锁时间量级,没有被后来的读者无限推后。这是上面“排队后新读者阻塞”规则的直接结果。

读写锁什么时候值得

实测 · 本机执行输出
  持有      0 ns:RWMutex.RLock  8 个 goroutine,总耗时    134.1 ms(1600000 次)
  持有      0 ns:Mutex.Lock     8 个 goroutine,总耗时    190.2 ms(1600000 次)
  持有    100 ns:RWMutex.RLock  8 个 goroutine,总耗时    227.8 ms(1600000 次)
  持有    100 ns:Mutex.Lock     8 个 goroutine,总耗时    578.2 ms(1600000 次)
  持有   1000 ns:RWMutex.RLock  8 个 goroutine,总耗时      3.1 ms(16000 次)
  持有   1000 ns:Mutex.Lock     8 个 goroutine,总耗时     18.8 ms(16000 次)
  持有 100000 ns:RWMutex.RLock  8 个 goroutine,总耗时    204.9 ms(16000 次)
  持有 100000 ns:Mutex.Lock     8 个 goroutine,总耗时   1661.8 ms(16000 次)

临界区几乎为空时,RWMutex 只比 Mutex 快 1.4 倍(134.1 对 190.2 ms);临界区 100 µs 时快约 8.1 倍(204.9 对 1661.8 ms)。再加上 RWMutex 本身更重,读很短的场景要先量。atomic.Value 那一篇里测过:8 个读者下 RWMutex.RLock 每次也要 78.8 ns。

读文档

实测 · 本机执行输出
  If any goroutine calls RWMutex.Lock while the lock is already held by one or more readers, concurrent calls to RWMutex.RLock will block until the writer has acquired (and released) the lock, to ensure that the lock eventually becomes available to the writer. Note that this prohibits recursive read-locking. A RWMutex.RLock cannot be upgraded into a RWMutex.Lock, nor can a RWMutex.Lock be downgraded into a RWMutex.RLock. 

文档那段话里有三个要点:写者排队后读者阻塞,所以递归读锁被禁止;RLock 不能升级为 Lock;Lock 也不能降级为 RLock。实验涉及了前两点,没有单独测“升级”会怎样(预期同样是死锁,没验证)。

分步演示

RWMutex
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。递归读锁与时间线实验在 GOMAXPROCS=8 下运行一次,输出为运行时 fatal error 的第一行;写者等待用 8 个持锁约 1 ms 的读者(忙等待)和 30 次写锁请求;临界区对比用忙等待模拟持有时间,总次数 160 万(持有 1000 ns 以上为 1.6 万),单次测量;文档引自 go doc sync.RWMutex
    • 没有验证:RLock 升级为 Lock 的结果、读者数量更多时写者等待的上限、RWMutex 的内部实现(没有读源码)、TryLock 的更多情形、其它架构、有大量写者时的公平性
    京ICP备2026067502号-1京公网安备11010502063487号