当读远多于写、读不修改数据且耗时短时才用RWMutex;它允许多读但写独占,误用会导致写饥饿或竞态。

什么时候该用 RWMutex 而不是 Mutex
当读操作远多于写操作,且读操作本身耗时较短、不修改共享数据时,RWMutex 才有实际收益。它允许并发读,但写操作会独占锁——这和 Mutex 的“读写全阻塞”不同。
常见误用是:读操作里偷偷改了字段、或读逻辑包含网络调用/数据库查询等长耗时动作,这时用 RWMutex 不仅没提升性能,反而因锁粒度错觉掩盖了竞争问题。
- ✅ 适合:
map[string]int缓存查表、配置快照读取、状态只读遍历 - ❌ 不适合:读函数里调用
sync.Map.Store()、在RLock()后 deferRUnlock()却忘了写逻辑可能 panic 导致漏解锁 - ⚠️ 注意:
RWMutex不是无成本的——它比Mutex多维护读计数,高并发读+频繁写会导致写饥饿(writer starvation)
RWMutex 的写操作会阻塞所有新读请求吗
会。一旦某个 goroutine 调用 Lock(),后续任何 RLock() 都会被阻塞,直到当前写完成并调用 Unlock()。但已获得 RLock() 的读 goroutine 可以继续执行,不会被中断。
这意味着:不能靠“先抢到读锁就能躲过写锁”来规避写等待——新读请求会在写锁持有期间排队,可能拖慢整体响应。
- 写操作开始前,所有未进入
RLock()的读请求都会卡住 - 已有读锁未释放完时,写锁会等待,此时写操作延迟不可控
- 若读操作耗时波动大(比如含日志打印或条件 sleep),更容易引发写饥饿
为什么 sync.RWMutex 没有 TryRLock
Go 标准库故意没提供 TryRLock() 或类似非阻塞读锁接口,因为它的语义难以定义清楚:是“立即失败”还是“尝试获取但不排队”?而真实场景中,读操作通常不该因抢不到锁就跳过——那意味着数据可能过期或逻辑断裂。
如果你真需要避免阻塞,常见做法是用 context.WithTimeout 包一层,配合 runtime.Gosched() 让出时间片,或者换用更轻量的方案:
- 对纯只读数据,考虑用
atomic.Value替代(如存储指针指向不可变结构) - 需部分更新时,用“写时复制(copy-on-write)”模式:新建副本 → 修改 → 原子替换指针
- 极端情况用
sync.Map,但它不适用于需要强一致遍历的场景
一个典型但容易翻车的 RWMutex 写法
下面这段代码看似合理,实则存在竞态和死锁风险:
func (c *Cache) Get(key string) (int, bool) {
c.mu.RLock()
defer c.mu.RUnlock() // 这里 defer 在 panic 时可能不执行!
v, ok := c.data[key]
if !ok {
return 0, false
}
// 假设这里有个隐藏副作用:记录访问次数(错误!)
c.accessCount++ // ❌ 在 RLock 下写共享变量
return v, true
}
问题不止一处:
-
c.accessCount++是写操作,却在RLock()下执行,触发未定义行为 -
defer c.mu.RUnlock()在函数 panic 时不会运行,导致锁永远不释放 - 正确做法是把写逻辑拆出去,或统一用
Lock(),或把计数器单独用sync/atomic
真正安全的读写分离,往往需要明确划分“只读路径”和“可写路径”,而不是靠锁类型模糊责任边界。










