sync.Mutex 与 RWMutex
本教程共 80 篇 · 第 62 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
62. sync.Mutex 与 RWMutex
本节目标:学会用
sync.Mutex保护共享数据,掌握读写锁sync.RWMutex的使用场景,理解死锁原因和避免方法。
为什么需要锁
上一节讲过数据竞争。多个 goroutine 同时改一个变量会出问题。channel 是通过通信解决,锁是另一种方式—直接把共享数据保护起来,同一时刻只允许一个 goroutine 操作。
sync.Mutex 互斥锁
sync.Mutex 是最基础的锁,只有两个方法:
Lock():加锁Unlock():解锁
加锁后,别的 goroutine 再调 Lock 会阻塞,直到锁被释放。
package main
import (
"fmt"
"sync"
)
var (
count int
mu sync.Mutex
)
func main() {
var wg sync.WaitGroup
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count) // 永远是 1000
}
对比上一节没加锁的版本,这次结果稳定是 1000。
用 defer 解锁
如果加锁和解锁之间有多个 return 路径,容易忘了解锁。习惯写法是用 defer:
func update() {
mu.Lock()
defer mu.Unlock()
count++
if count > 100 {
return // 不会忘解锁,defer 兜底
}
count += 10
}
Tip
defer会在函数返回时自动解锁。但要注意defer也有微小开销,在极高性能要求的循环里可以手动Unlock。大多数场景defer是首选,安全第一。
互斥锁不可重入
Go 的 Mutex 不是递归锁(不可重入)。同一个 goroutine 重复 Lock 会死锁:
func a() {
mu.Lock()
defer mu.Unlock()
b() // b 里又 Lock,死锁!
}
func b() {
mu.Lock()
defer mu.Unlock()
fmt.Println("b")
}
a 持有锁后调 b,b 又想拿同一把锁,永远等不到。
WarningGo 故意不做可重入锁。如果你发现自己需要重入,多半是设计有问题。考虑把锁的范围拆小,或者把需要锁的逻辑拆成不带锁的内部函数。
sync.RWMutex 读写锁
如果读多写少,用 sync.Mutex 会让读操作也互相排队,浪费。sync.RWMutex 解决这个问题:
- 多个 goroutine 可以同时持有读锁
- 写锁是排他的,和任何读锁、写锁互斥
var (
data map[string]string
rwmu sync.RWMutex
)
func read(key string) string {
rwmu.RLock() // 读锁
defer rwmu.RUnlock()
return data[key]
}
func write(key, val string) {
rwmu.Lock() // 写锁
defer rwmu.Unlock()
data[key] = val
}
读写锁适合「读远多于写」的场景,比如配置缓存、查询服务。
RWMutex 的方法
rwmu.RLock() // 加读锁
rwmu.RUnlock() // 解读锁
rwmu.Lock() // 加写锁
rwmu.Unlock() // 解写锁
还有两个非阻塞的尝试方法(Go 1.18+):
if rwmu.TryRLock() { // 尝试加读锁,失败返回 false 不阻塞
// 拿到了
rwmu.RUnlock()
}
if rwmu.TryLock() { // 尝试加写锁
// 拿到了
rwmu.Unlock()
}
Note
TryLock只在特殊场景用,比如你想抢锁但抢不到就跳过。正常逻辑还是用阻塞式的Lock。
读锁别滥用
读写锁不是万金油。如果你的读写比例差不多,RWMutex 反而比 Mutex 慢,因为管理读写锁状态本身有开销。经验值:读占比超过 80% 才值得用读写锁。
另外,持有读锁时别做耗时操作,否则会拖慢想拿写锁的 goroutine。
死锁的常见原因
死锁就是 goroutine 之间互相等,谁都不让步。常见几种:
1. 重复加锁(前面讲过)
2. 锁顺序不一致
两个 goroutine 各自拿了不同的锁,又想拿对方的锁:
// goroutine A: lock1 -> lock2
// goroutine B: lock2 -> lock1
// 互相等,死锁
解决办法:所有地方按相同顺序加锁。
3. 等 channel 却没人发
func main() {
ch := make(chan int)
mu.Lock()
ch <- 1 // 阻塞,但锁还拿着
mu.Unlock()
}
Warning持有锁时不要做可能阻塞的操作(channel 收发、IO、sleep)。锁的临界区要尽量短,只保护共享数据的读写。
锁要传指针
sync.Mutex 是结构体,复制后是另一把锁,保护不了同一个数据:
// 错误:复制了锁
func bad(wg sync.WaitGroup) { // 应该传 *sync.WaitGroup
defer wg.Done()
}
sync.Mutex、sync.WaitGroup 等都包含内部状态,必须用指针传递或作为结构体的指针字段。
Tip可以用
go vet检查锁复制的问题,它会报assignment copies lock value之类的警告。
小结
sync.Mutex:互斥锁,Lock/Unlock保护临界区sync.RWMutex:读写锁,读多写少场景用,RLock/RUnlock共享读- 习惯用
defer Unlock,避免忘解锁 - Go 锁不可重入,重复加锁会死锁
- 锁顺序要一致,临界区要短,持锁别阻塞
- 锁必须传指针,不能复制
下一节讲 sync.WaitGroup 和 sync.Once。