★ sync.Mutex 与 sync.RWMutex 的原理与用法要点是什么?
结论先行
sync.Mutex 是互斥锁,保证同一时刻只有一个 goroutine 进入临界区;sync.RWMutex 在其上扩展出“多读单写”:读者之间共享、写者独占,且写者优先。两者的实现都基于原子 CAS、自旋与信号量睡眠队列,并引入“饥饿模式”保证公平,防止长时间等锁的 goroutine 被饿死。
要点
- 状态机:锁内部用一个 state 字段,低 1 位是锁定标志,其余位记录等待者数量与是否处于饥饿模式。
- 正常模式:解锁时唤醒一个等待者,新来的竞争者可以和它一起抢锁(自旋 + CAS),吞吐高但可能不公平。
- 饥饿模式:等待者等待超过约 1ms 即切换;此后新请求不再抢锁、直接排到队尾,保证先到先得,消除长尾饿死。
- 自旋条件:多核(GOMAXPROCS>1)且仍有可运行 P 时,先短暂自旋几次再进睡眠,减少线程切换开销。
- 用法铁律:Lock 后必须 Unlock,推荐 defer 收尾;临界区尽量小;锁不可复制(拷贝后状态分裂,go vet 报 copylocks)。
- 不可重入:同一 goroutine 对已持有的 Mutex 再次 Lock 会死锁;Go 的锁没有可重入语义,别拿“递归锁”的思维套。
- RWMutex 语义:RLock/RUnlock 可并发;有写者在等待时,新读者会排队让行,避免写者被读者无限饿死。
- RWMutex 注意:读锁也不能随意嵌套重入——有写者排队时,同一 goroutine 的第二次 RLock 可能把自己阻塞死。
示例
var mu sync.RWMutex
cache := map[string]string{}
func Get(k string) string {
mu.RLock() // 读者并发
defer mu.RUnlock()
return cache[k]
}
func Set(k, v string) {
mu.Lock() // 写者独占
defer mu.Unlock()
cache[k] = v
}
常见追问/记忆点
- 追问:读写锁一定更快吗?答:读多写少才划算;写多时锁竞争与维护开销可能反而不如普通 Mutex。
- 追问:为什么 Mutex 不能复制?答:拷贝出来的锁状态独立,会出现“两把锁”导致临界区失效,vet 会拦截。
- 追问:正常模式与饥饿模式谁好?答:正常模式吞吐高但有公平风险,饥饿模式公平但略降吞吐,运行时自动切换。
- 记忆:Lock/Unlock 成对、defer 收尾、临界区切小;RWMutex 写优先防写者饿死。