sync.RWMutex在读多写少场景下性能优于sync.Mutex,但RLock/RUnlock必须严格配对,漏调或错序会导致写锁永久阻塞、goroutine卡死;写占比超15%~20%时反不如sync.Mutex,且禁止跨函数传递读锁、混用锁须统一顺序。

sync.RWMutex 在读多写少场景下性能优于 sync.Mutex,但用错一步就可能引发隐形死锁——不 panic、不报错、CPU 归零、goroutine 数稳定不降,只等你花半天查 pprof。
RLock 和 RUnlock 必须严格配对
sync.RWMutex 不检查加锁/解锁次数是否匹配。同一个 goroutine 多次调用 RLock() 却没对应次数的 RUnlock(),会导致后续所有 Lock() 永远阻塞。
- 常见错误:在函数入口无条件
mu.RLock(),然后用defer mu.RUnlock(),但中间因context.Err()提前 return,defer仍会执行 → 解锁多于加锁 → 该 goroutine 的读锁计数变负 → 写锁被永久封禁 - 正确做法:只在
RLock()成功后才注册defer mu.RUnlock(),例如:mu.RLock() defer mu.RUnlock() // 紧跟 RLock 后,同一作用域 if ctx.Err() != nil { return // 不影响 defer 执行 } - 更安全写法:把读锁逻辑包进独立作用域,避免跨分支干扰
写操作占比超 20% 时,sync.RWMutex 反而更慢
sync.RWMutex 内部状态切换(读→写等待→写→读恢复)开销明显。实测中,当写请求频率超过总并发操作的 15%~20%,其吞吐反而低于 sync.Mutex。
- 场景判断:配置热更新、指标打点、缓存淘汰等看似“写少”的逻辑,若单位时间触发频次高(如每秒数十次),实际已越界
- 性能陷阱:读路径里隐含写行为(比如
Get(key)中顺手更新lastAccess[key]),会让读锁退化为写锁竞争源 - 替代方案:写操作强一致性要求不高时,改用
atomic.Value+ 双缓冲;写占比高且需强一致,直接换回sync.Mutex
多个 RWMutex 加锁顺序必须统一
和 sync.Mutex 一样,多个 sync.RWMutex 间加锁顺序不一致会触发 AB-BA 循环等待。运行时不报错,现象是部分 goroutine 卡在 (<em>RWMutex).RLock</em>,部分卡在 (RWMutex).Lock,数量稳定不减。
- 错误模式:函数 A 先锁
muUser再锁muOrder,函数 B 反过来 - 安全做法:按变量名字符串排序(如
muOrdermuUser),或按内存地址排序:if uintptr(unsafe.Pointer(&mu1)) < uintptr(unsafe.Pointer(&mu2)) { mu1.RLock() mu2.RLock() } else { mu2.RLock() mu1.RLock() } - 禁止跨函数传递已加读锁的结构体:不要把加了
RLock()的对象传给其他函数再解锁,容易漏或乱序
RLock 后 panic 会导致本次读锁永远泄漏,前提是 defer mu.RUnlock() 没写在同一作用域内。这是最易被忽略的点:panic 发生在 RLock() 之后、defer 注册之前,那这次加锁就再没人能解。调试时看 pprof 的 goroutine stack,若大量停在 (*RWMutex).Lock 且调用栈含业务读逻辑,优先盯这里。


















