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。实验涉及了前两点,没有单独测“升级”会怎样(预期同样是死锁,没验证)。