AIGC标识 sync.Mutex 互斥锁

sync.Mutex 互斥锁


一、为什么需要互斥锁

当多个 goroutine 同时读写同一个变量时,会发生数据竞争(data race)。比如银行账户的例子:

var balance int

func Deposit(amount int) {
    balance = balance + amount // 这不是原子操作!
}

balance = balance + amount 在 CPU 层面至少拆成三步:

  1. 从内存读取 balance 到寄存器
  2. 寄存器值加上 amount
  3. 将结果写回内存

两个 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
posted @ 2026-07-27 08:59  FfHUCisI  阅读(0)  评论(0)    收藏  举报