readerCount是复用为状态信号量的有符号32位整数,低30位存活跃读者数,第31位标识写锁抢占;Lock()原子减rwmutexMaxReaders而非清零,readerWait仅写锁入队时赋值且不自动清零,readerSem共用导致写锁释放后仅唤醒一个读锁等待者。

readerCount 是有符号的 32 位整数,不是计数器那么简单
很多人以为 readerCount 就是“当前有多少 goroutine 持有读锁”,其实它被复用为状态信号量:低 30 位存活跃读者数,最高位(第 31 位)被写锁抢占时置为 1,使其整体变负。Go 1.8 后的实现中,Lock() 并不直接清零 readerCount,而是原子减去 rwmutexMaxReaders(即 1 ),让其瞬间跌入负区间。
这意味着:
-
readerCount >= 0仅表示「当前无排队写锁」,不保证「此刻没写锁在执行」——写锁可能刚释放、尚未唤醒 reader readerCount 表示「至少有一个写锁已调用Lock()并进入等待队列」,哪怕它还没拿到锁- 每次
RLock()都会先atomic.AddInt32(&rw.readerCount, 1),再判断结果是否 readerSem
写锁排队不等于立即抢占,但会立刻拦截新读锁
当 goroutine A 调用 Lock() 时,它首先尝试获取内部的 w(一个 sync.Mutex),成功后才修改 readerCount 并设置等待态。这个过程有微小窗口:A 已排队但尚未完成状态更新,B 此时调用 RLock() 仍可能通过快路径(readerCount 还未变负)——但只要 A 完成原子减操作,所有后续 RLock() 都会被拦下。
关键点在于:写锁的“排队生效”和“执行开始”是两个阶段,而读锁的拦截只依赖前者。这导致一种常见误判:
- 你看到多个
GetString并发调用,其中某个嵌套了Get(),后者又调用RLock() - 此时若有任意 goroutine 正在排队写(哪怕还没拿到锁),那个嵌套的
RLock()就会永久阻塞在runtime_SemacquireMutex(&rw.readerSem, false, 0) - 这不是死锁,是明确的调度策略:写优先,且不可绕过
嵌套 RLock() 在 RWMutex 下天然危险
Go 的 sync.RWMutex 不支持重入,也不做 goroutine 级别持有者校验。同一个 goroutine 连续两次 RLock() 是语法合法的,但第二次调用会受全局 readerCount 状态影响——哪怕第一次锁还持有着,只要期间有写锁排队,第二次就挂起。
典型踩坑代码长这样:
func (s *Store) Get(key string) string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.getInternal(key)
}
func (s *Store) getInternal(key string) string {
s.mu.RLock() // ❌ 这里可能永远等下去
defer s.mu.RUnlock()
// ...
}
解决方法不是加 TryRLock(),而是重构调用链,确保锁边界清晰。更安全的做法是把锁提到最外层,或改用 sync.Mutex + 显式读缓存,避免对 RWMutex 的语义产生“读锁可无限嵌套”的错觉。
readerWait 和 readerSem 的唤醒逻辑容易被忽略
readerWait 只在写锁排队时使用:它记录「在写锁入队那一刻,还有多少个活跃读者未释放」。每次 RUnlock() 会检查是否自己是最后一个读者(即 readerCount == 0 && readerWait > 0),若是,就唤醒一个写锁等待者。
这里有个隐含约束:readerWait 不会自动清零,它只由写锁入队时一次性赋值。如果写锁排队后又取消(比如超时返回),readerWait 仍残留,可能导致后续写锁被错误唤醒。不过标准库没暴露取消写锁的 API,所以实践中几乎不会遇到——但自定义封装时必须注意这点。
真正容易出问题的是 readerSem:它被所有因写锁排队而阻塞的读锁共用。一旦写锁释放,它只唤醒一个等待者(不是全部),其余仍卡着——直到下一个写锁来或上一个读锁释放触发新一轮唤醒。这种“逐个放行”机制进一步放大了写锁高优先级带来的读延迟波动。










