sync.Mutex 互斥锁
sync.Mutex 互斥锁
一、为什么需要互斥锁
当多个 goroutine 同时读写同一个变量时,会发生数据竞争(data race)。比如银行账户的例子:
var balance int
func Deposit(amount int) {
balance = balance + amount // 这不是原子操作!
}
balance = balance + amount 在 CPU 层面至少拆成三步:
- 从内存读取 balance 到寄存器
- 寄存器值加上 amount
- 将结果写回内存
两个 goroutine 并发执行 Deposit 时,可能出现交错,导致某笔存款"丢失"。这就是数据竞争。
二、sync.Mutex 的基本用法
Mutex(互斥锁)保证同一时刻只有一个 goroutine 能进入临界区(critical section)。
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock() // 获取锁,若已被占用则阻塞等待
balance += amount // 临界区:只有持锁者可以执行
mu.Unlock() // 释放锁
}
约定俗成:被 mutex 保护的变量紧跟在 mutex 声明之后。这种编排让阅读者一眼就知道"这些变量被这把锁保护"。
三、defer 释放锁
在复杂函数中,可能有多个 return 路径,人工保证每个路径都 Unlock 很容易遗漏。defer 完美解决:
func Balance() int {
mu.Lock()
defer mu.Unlock() // 函数退出前自动释放,即使发生 panic 也会执行
return balance
}
defer 的代价极低,远小于它带来的代码安全和可读性收益。永远优先使用 defer 释放锁。
四、Go 的 Mutex 不可重入
这是 Go 与 Java 的一个重要区别。Java 的 ReentrantLock 允许同一个线程多次获取同一把锁;Go 的 sync.Mutex 不允许。
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
Deposit(-amount) // 死锁!Deposit 内部又会 Lock 同一个 mu
// ...
}
解决方案:将实际逻辑拆分到不导出的内部函数(假设锁已被持有):
func deposit(amount int) { balance += amount } // 不导出,调用者负责持锁
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false
}
return true
}
这种"导出函数加锁,内部函数干活"的模式在 Go 标准库中很常见。
五、Mutex 的零值可用
sync.Mutex 的零值就是一个未锁定的锁,不需要 New 或 Init。这意味着你可以直接把 Mutex 嵌入结构体,声明后就能用。
六、练习代码
保存为 mutex_demo.go,编译运行:
package main
import (
"fmt"
"sync"
"time"
)
// 银行账户 — 使用 Mutex 保护共享变量
type BankAccount struct {
mu sync.Mutex
balance int64
}
// 存款(导出函数,负责加锁)
func (a *BankAccount) Deposit(amount int64) {
a.mu.Lock()
defer a.mu.Unlock()
a.deposit(amount)
}
// 取款
func (a *BankAccount) Withdraw(amount int64) bool {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance < amount {
return false
}
a.deposit(-amount)
return true
}
// 查询余额
func (a *BankAccount) Balance() int64 {
a.mu.Lock()
defer a.mu.Unlock()
return a.balance
}
// 内部存款(假设调用者已持锁)
func (a *BankAccount) deposit(amount int64) {
a.balance += amount
}
func main() {
account := &BankAccount{}
var wg sync.WaitGroup
// 模拟 100 个 goroutine 同时存款 1 元
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
account.Deposit(1)
}()
}
// 同时进行 50 次取款尝试
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
account.Withdraw(1)
}()
}
wg.Wait()
fmt.Printf("最终余额: %d (期望: 50)\n", account.Balance())
// 100次存1元 + 50次取1元 = 50元
}
运行结果(无数据竞争):
最终余额: 50 (期望: 50)
七、关键要点
| 要点 | 说明 |
|---|---|
| 临界区 | Lock 和 Unlock 之间的代码,同一时刻只有一个 goroutine 执行 |
| defer Unlock | 首选模式,防止忘记释放,panic 时也能释放 |
| 不可重入 | Lock 之后不能再次 Lock 同一把锁 |
| 拆分模式 | 导出函数加锁,内部函数干活(命名常小写开头) |
| 零值可用 | var mu sync.Mutex 即可使用,无需初始化 |
| 粒度控制 | 临界区应该尽量小,不要在持锁时做 I/O |

浙公网安备 33010602011771号