sync.rwmutex适用于读多写少且读操作耗时较长的场景,可降低读竞争;但读极短时sync.mutex更轻量,单值操作优先用atomic,需防死锁、写饥饿及误用。

sync.RWMutex 什么时候该用,而不是直接上 sync.Mutex
读多写少的场景下,sync.RWMutex 能显著降低读操作的锁竞争开销——多个 goroutine 同时读不会互斥,只有写与写、写与读才阻塞。但别一看到“读多”就无脑换,它本身有额外调度开销;如果读操作极短(比如只读一个 int 字段),用 sync.Mutex 反而更轻量。
常见误判点:以为“只要读比写多就一定赢”,其实要看读操作耗时和并发读 goroutine 数量。压测前先用 go tool trace 看锁等待时间,比凭感觉靠谱。
ReadLock / WriteLock 的典型误用模式
最常踩的坑是忘记释放锁,或在 defer 中写错顺序:
-
rwmu.RLock()后漏掉defer rwmu.RUnlock()→ 后续所有写操作永久阻塞 - 在函数开头
rwmu.RLock(),中间又调了另一个可能也拿rwmu的函数 → 死锁(RLock不可重入) - 用
rwmu.Lock()代替rwmu.RLock()做纯读操作 → 人为制造读竞争,完全浪费RWMutex优势
实操建议:所有 RLock() 必须紧跟着 defer RUnlock(),且不能跨 goroutine 传递锁状态;写操作优先走 Lock()/Unlock(),别试图用 RLock()+Unlock() 混用。
读写锁和原子操作(atomic)怎么选
不是所有读多写少都适合 sync.RWMutex。如果只是单个基础类型(如 int64、*struct 指针、bool)的读写,atomic.LoadInt64 / atomic.StoreInt64 更快、无锁、无内存分配。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
但注意边界:
-
atomic不支持结构体整体读写(除非用unsafe.Pointer手动包装,风险高) - 复合操作(如“读A→算B→写C”)无法靠
atomic保证原子性,必须用锁 -
sync.RWMutex保护的是临界区逻辑,atomic保护的是单个值——选哪个,取决于你“保护什么”
示例:缓存命中计数器用 atomic.Int64;整个配置结构体的热更新用 sync.RWMutex。
性能陷阱:RWMutex 在写饥饿下的实际表现
sync.RWMutex 默认不保证公平性。持续高频写请求(哪怕每次只写几毫秒)会导致读操作无限排队——goroutine 看似没卡住,但 RLock() 一直拿不到,响应延迟飙升。
缓解方式有限:
- 写操作尽量短,避免在
Lock()内做网络调用、大循环或阻塞 I/O - 考虑用
sync.Map替代“读远多于写 + key 稳定”的映射场景(它内部做了读优化) - 真有强公平需求,得自己基于
sync.Cond实现带队列的读写锁,但成本高,慎入
真正难处理的,从来不是怎么加锁,而是写操作里藏了多少没意识到的阻塞点——查日志、看 trace、盯住 runtime/pprof 的 goroutine 阻塞堆栈,比背 API 重要得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










