读多写少场景用RWMutex因允许多读一写,提升并发吞吐;但写频繁或读极短时Mutex更优,且RWMutex不保证读写绝对顺序、不支持TryLock,复合操作需双检锁等上层保障。

为什么读多写少场景下要用 RWMutex 而不是 Mutex
因为 RWMutex 允许多个 goroutine 同时读,但写操作会独占锁;而 Mutex 无论读写都互斥。在读远多于写的场景(比如配置缓存、状态快照),用 RWMutex 能显著提升并发吞吐。但要注意:它不保证读写之间的“绝对顺序”,比如一个写操作正在等待时,新来的读请求仍可能插队获取读锁——这不是 bug,是设计使然。
RWMutex 的四个核心方法怎么配对使用
必须严格区分读/写路径,否则会死锁或数据竞争:
-
RLock()和RUnlock()成对用于只读操作;不能在持有Rlock期间调用Lock() -
Lock()和Unlock()成对用于写操作;此时所有新RLock()会被阻塞,直到Unlock() - 禁止嵌套:不能在已持
RLock()的 goroutine 中再调Lock(),这会永久阻塞 - 忘记
Unlock()或RUnlock()是常见 panic 来源,建议用defer mu.RUnlock()保底
什么时候 RWMutex 反而比 Mutex 更慢
当写操作频繁(比如每秒数百次以上)或读操作极短(纳秒级),RWMutex 的额外状态管理开销(如维护 reader 计数、唤醒策略)可能超过收益。实测发现,在写占比 >15% 且临界区极小时,Mutex 往往更稳。另外,RWMutex 不支持 TryLock 类语义,无法做非阻塞探测,这点常被忽略。
一个易错的典型误用模式
下面这段代码看似合理,实则危险:
立即学习“go语言免费学习笔记(深入)”;
func (c *Cache) Get(key string) string { c.mu.RLock() v, ok := c.data[key] c.mu.RUnlock() if !ok { // 触发写:但此时 mu 已释放! c.mu.Lock() defer c.mu.Unlock() if _, loaded := c.data[key]; !loaded { c.data[key] = fetchFromDB(key) } return c.data[key] } return v }
问题在于:两次锁之间存在竞态窗口——另一个 goroutine 可能在 RUnlock() 后、Lock() 前删掉该 key。正确做法是用双检锁(double-checked locking),或改用 sync.map 处理高频读写混合场景。RWMutex 本身不解决“读-判断-写”这类复合操作的原子性,得靠上层逻辑兜底。