RWMutex仅在读多写少时优于Mutex;写频繁时因原子操作、等待队列等开销反而更慢,50%读写混合下吞吐量低15–30%。
RWMutex不是万能加速器——它只在读操作远多于写操作的场景下比 sync.Mutex 有明显优势。一旦写操作占比升高(比如每10次读就有3次写),sync.RWMutex 的内部原子计数、读锁等待队列管理、writer-preferring 策略等开销会反超普通互斥锁。
readerCount 置为负值并等待所有活跃读锁释放,这个过程比 Mutex.Lock() 多出至少2次原子操作 + 可能的 goroutine 唤醒延迟RWMutex 吞吐量可能比 Mutex 低 15–30%读锁不阻塞其他读,但会阻塞所有写。如果 RLock() 持有时间过长(比如读完 map 后又去调用 HTTP 请求、序列化 JSON 或遍历大 slice),等于人为延长了写操作的等待窗口。
RLock() 后未立即 RUnlock(),而是包裹了业务逻辑甚至 IO 调用gopark,pprof 显示大量 sync.runtime_SemacquireRWMutexR 阻塞态func getConfig(key string) string {
rwMu.RLock()
v := configMap[key] // ✅ 纯读
rwMu.RUnlock() // ⚠️ 必须立刻释放
// ❌ 不要在这里做:json.Marshal(v), http.Get(...), time.Sleep(10ms)
return v
}
写锁升级失败是性能隐形杀手
Go 的 sync.RWMutex 明确不支持“读锁升级为写锁”。常见错误模式是:先 RLock() 查数据,发现不存在再 RUnlock() → Lock() → 写入。这看似合理,实则埋下三重隐患:
RUnlock() 后、Lock() 前完成写入,导致
Lock() 后没 defer Unlock() 就永久死锁真正安全的做法只有两种:
func getOrSet(key, def string) string {
// 第一次检查(读锁)
rwMu.RLock()
if v, ok := cache[key]; ok {
rwMu.RUnlock()
return v
}
rwMu.RUnlock()
// 二次检查(写锁)
rwMu.Lock()
if v, ok := cache[key]; ok { // ✅ 再查一次,防竞争
rwMu.Unlock()
return v
}
cache[key] = def
rwMu.Unlock()
return def}
复制 RWMutex 或跨 goroutine 传指针 = 随机崩溃
sync.RWMutex 是非复制类型(non-copyable)。一旦被结构体字段赋值、函数参数传值、或 append 到切片,就触发隐式复制——两个副本各自维护独立的 readerCount 和信号量,锁状态完全脱节。
fatal error: all goroutines are asleep - deadlock
-race 编译,运行时会报 WARNING: DATA RACE 并定位到复制点*sync.RWMutex),且确保初始化后不再复制最容易忽略的一点:struct 字段声明为 mu sync.RWMutex(而非 mu *sync.RWMutex)时,只要该 struct 被赋值或作为参数传入,就已悄悄复制。