3. 互斥锁 Mutex:sync.Mutex 基本用法、临界区保护、Lock 与 Unlock 配对原则
3.1 为什么需要互斥锁
在 Go 并发编程中,当多个 goroutine 同时访问同一共享变量时,会发生数据竞态(Data Race)。互斥锁 sync.Mutex 是 Go 标准库提供的最基础的同步原语,用于保证同一时刻只有一个 goroutine 能进入临界区,从而保护共享数据的一致性。
核心概念:互斥锁(Mutual Exclusion,Mutex)是一种排他锁。当一个 goroutine 持有锁时,其他试图获取同一锁的 goroutine 会阻塞等待,直到锁被释放。
3.2 sync.Mutex 基本用法
sync.Mutex 提供了两个方法:Lock() 和 Unlock()。使用模式通常为:
var mu sync.Mutex
var counter int
func increment() {
mu.Lock() // 加锁
counter++ // 临界区操作
mu.Unlock() // 解锁
}
3.3 临界区保护原则
临界区是指访问共享资源的代码片段。保护临界区需遵循以下原则:
- 最小化原则:临界区代码应尽可能短,只包含必要的共享资源操作,避免在锁内执行 I/O、网络请求等耗时操作。
- 一致性原则:所有对共享变量的读写操作都必须加锁保护,不能有的地方加锁、有的地方不加锁。
- 不可嵌套原则:Go 的
sync.Mutex是不可重入的,同一 goroutine 不能对已持有的 Mutex 再次Lock(),否则会导致死锁。
常见陷阱:忘记解锁、在分支/异常路径中未解锁、锁拷贝(Mutex 是值类型,传递副本会导致锁失效)。
3.4 Lock 与 Unlock 配对原则
配对原则是互斥锁使用的黄金法则:Lock() 和 Unlock() 必须成对出现,且每个 Lock() 最终必须对应一个 Unlock()。推荐使用 defer 确保解锁:
func safeUpdate(data *map[string]int, key string, value int) {
mu.Lock()
defer mu.Unlock() // 即使函数 panic 也能解锁
(*data)[key] = value
}
下表总结了配对原则的关键检查点:
| 检查维度 | 正确做法 | 错误示例 |
|---|---|---|
| 数量配对 | 每个 Lock 对应一个 Unlock | Lock 两次但只 Unlock 一次 |
| 路径覆盖 | 所有 return/panic 路径都解锁 | if 分支中 return 前忘记 Unlock |
| 锁与 goroutine | 谁加锁谁解锁 | goroutine A 加锁,goroutine B 解锁 |
| 锁拷贝 | 传递指针 *sync.Mutex | 按值传递 sync.Mutex |
3.5 互斥锁工作流程
下图展示了两个 goroutine 通过互斥锁访问共享资源的完整流程:
3.6 典型使用模式
以下是几种常见的 Mutex 使用模式:
模式一:结构体封装
type SafeCounter struct {
mu sync.Mutex
value int
}
func (sc *SafeCounter) Inc() {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.value++
}
func (sc *SafeCounter) Value() int {
sc.mu.Lock()
defer sc.mu.Unlock()
return sc.value
}
模式二:批量操作保护
type SafeMap struct {
mu sync.Mutex
m map[string]int
}
func (sm *SafeMap) BatchUpdate(updates map[string]int) {
sm.mu.Lock()
defer sm.mu.Unlock()
for k, v := range updates {
sm.m[k] = v
}
}
性能提示:锁的粒度直接影响并发性能。粗粒度锁(一把锁保护整个数据结构)实现简单但并发度低;细粒度锁(如分段锁、读写锁)能提升性能但复杂度增加。在性能敏感场景,可考虑使用
sync.RWMutex 或原子操作替代。
3.7 常见错误与避坑指南
- 死锁:同一 goroutine 重复 Lock;多个 goroutine 互相等待对方释放锁。
- 锁拷贝:将
sync.Mutex作为函数参数传递(值拷贝),导致锁失效。 - 遗漏解锁:在复杂条件分支中忘记调用
Unlock(),导致其他 goroutine 永久阻塞。 - 锁外操作:在临界区外访问共享变量,造成数据竞态。
下表对比了 Mutex 与后续章节将介绍的原子操作:
| 特性 | sync.Mutex | 原子操作(atomic) |
|---|---|---|
| 适用场景 | 复杂临界区、多字段更新 | 简单计数器、标志位 |
| 性能开销 | 较高(涉及系统调用和 goroutine 调度) | 极低(CPU 指令级) |
| 使用复杂度 | 需注意配对和死锁 | 简单,无死锁风险 |
| 可保护范围 | 任意代码块 | 仅单个变量 |
掌握 sync.Mutex 的基本用法和配对原则,是构建安全并发程序的基石。在实际开发中,应始终遵循最小临界区和 defer 解锁的最佳实践,避免常见陷阱。