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 通过互斥锁访问共享资源的完整流程:

sync.Mutex 互斥锁工作流程 sync.Mutex 互斥锁工作流程 Goroutine A Goroutine B 共享资源(临界区) 锁状态:空闲/持有 mu.Lock() mu.Lock() 阻塞 A 操作共享资源 mu.Unlock() B 获取锁,进入 说明:Goroutine A 先获取锁,B 阻塞等待;A 释放锁后 B 才能进入临界区

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 解锁的最佳实践,避免常见陷阱。