sync.Mutex 互斥锁

sync.Mutex 互斥锁


一、为什么需要互斥锁

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

go 复制代码
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)。

go 复制代码
var (
    mu      sync.Mutex
    balance int
)

func Deposit(amount int) {
    mu.Lock()         // 获取锁,若已被占用则阻塞等待
    balance += amount // 临界区:只有持锁者可以执行
    mu.Unlock()       // 释放锁
}

约定俗成:被 mutex 保护的变量紧跟在 mutex 声明之后。这种编排让阅读者一眼就知道"这些变量被这把锁保护"。

三、defer 释放锁

在复杂函数中,可能有多个 return 路径,人工保证每个路径都 Unlock 很容易遗漏。defer 完美解决:

go 复制代码
func Balance() int {
    mu.Lock()
    defer mu.Unlock() // 函数退出前自动释放,即使发生 panic 也会执行
    return balance
}

defer 的代价极低,远小于它带来的代码安全和可读性收益。永远优先使用 defer 释放锁

四、Go 的 Mutex 不可重入

这是 Go 与 Java 的一个重要区别。Java 的 ReentrantLock 允许同一个线程多次获取同一把锁;Go 的 sync.Mutex 不允许

go 复制代码
func Withdraw(amount int) bool {
    mu.Lock()
    defer mu.Unlock()
    Deposit(-amount) // 死锁!Deposit 内部又会 Lock 同一个 mu
    // ...
}

解决方案:将实际逻辑拆分到不导出的内部函数(假设锁已被持有):

go 复制代码
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,编译运行:

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
相关推荐
平头哥AI17 小时前
Day 01 | go run 跑通第一个 Go 程序,go build 留下一个能拷走的 exe
开发语言·后端·golang
2601_9622965118 小时前
如何使用Golang包路径_管理本地和远程模块引用
golang·数据转换·变量定义·常用库·编程语言区别
FfHUCisI1 天前
GMP 调度器:Go 并发的心脏是如何跳动的
开发语言·golang·php
我不会起名字3221 天前
一天一道算法题(26):栈的简单应用
java·数据结构·python·算法·leetcode·golang·
gsls2008081 天前
告别 Vault 的复杂度:用 Go 标准库给 Windows 凭据管理器装上 MCP
windows·golang·mcp
名字还没想好☜1 天前
Go 时间格式化为什么用 2006-01-02:time.Format/Parse 的参考时间、时区与解析踩坑
开发语言·后端·golang·go
FfHUCisI2 天前
Golang Map 哈希表实现
golang·哈希算法·散列表
源代码•宸2 天前
前置准备:定时微服务背景和现状
开发语言·经验分享·后端·微服务·云原生·架构·golang
李燚2 天前
把规则搬回家:三个 BC 的贫血→充血重构实录(第103篇)
golang·agent·ddd·领域驱动设计·eino·deepflux·eino adk
astronautyi2 天前
Go 运行时内存分配与 GC 位图深度剖析
开发语言·后端·golang