写锁能“插队”是因为RWMutex用readerCount符号位作调度开关:正值允许多读并发,负值(如Lock时减去极大值)标记写者待命,后续新读请求即被阻塞在readerSem上,从而为写锁创造获取机会。

sync.RWMutex 的写锁为什么能“插队”
因为 sync.RWMutex 不是“读-读不互斥,所以读可以随便嵌套”的简单模型。它用 readerCount 的正负号做调度开关:正值表示只有读者,负值(即 readerCount )表示已有写者在排队或已持有锁。一旦有 goroutine 调用 <code>Lock(),它会原子地把 readerCount 减去一个极大值(rwmutexMaxReaders),使其立刻变负——后续所有 RLock() 都会在 atomic.AddInt32(&rw.readerCount, 1) 判断中失败,直接挂起等待 <code>readerSem。
嵌套 RLock 为什么会卡住
这不是死锁,是明确的阻塞行为。典型场景是外层 GetString() 拿了读锁,中间调用 Get() 再次尝试 RLock(),但此时恰好有写操作(如 Set())已调用 Lock() 并使 readerCount 变负:
- 外层
RLock()成功时,readerCount是正数(比如 1) -
Set()调用Lock()→readerCount瞬间变成负数(如 -1073741823) - 内层
RLock()执行atomic.AddInt32(&rw.readerCount, 1),结果仍是负数 → 触发runtime_Semacquire(&rw.readerSem)挂起
关键点:只要写者进了队列,新读者就不再被允许“加塞”,哪怕当前没人在写。
Lock() 和 RLock() 的参数与返回值差异
sync.RWMutex 的这四个方法全部无参数、无返回值:
-
RLock():阻塞直到可获取读锁,不返回错误 -
RUnlock():必须与RLock()成对,否则 panic -
Lock():阻塞直到可获取写锁,不返回错误 -
Unlock():必须与Lock()成对,否则 panic
它们都不接受超时、上下文或重试策略。若需控制等待行为(如避免无限阻塞),必须自行封装,例如用 select + time.After 包裹调用,或改用带上下文的第三方锁实现。
写优先带来的实际影响和规避建议
写优先保障了写操作不被持续读压垮,但也让读路径变得脆弱——尤其在读操作内部再读、或读逻辑较长时,极易因外部写请求而卡住。常见规避方式:
- 避免在已持读锁的函数内再次调用同锁的
RLock();把共享数据拷贝出来再处理 - 读多写少场景下,若读逻辑复杂,考虑用
sync.Mutex替代,反而更可控 - 写操作本身应尽量轻量;重计算/IO 应在释放锁后进行
- 调试时注意:
go tool trace中能看到RLock()在semacquire阶段长时间休眠,这是写者排队的明确信号
最常被忽略的是:写优先不是“写更快”,而是“写一来,读就得排队”。这个语义在嵌套调用和长读路径中会突然暴露,且无 warning 或 panic 提示。


















