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
相关推荐
茉莉玫瑰花茶6 小时前
GO [ 数据格式 ]
服务器·网络·算法·golang
土豆.exe7 小时前
chunked 编码的行尾:Go、h11 与 Tomcat 各错在哪一处
开发语言·网络安全·golang·tomcat
茉莉玫瑰花茶1 天前
GO [ 标准库 ]
开发语言·后端·golang
紫洋葱hh2 天前
Go 入门基础:类型、变量与运算符
开发语言·后端·ai·golang
codeejun2 天前
每日一Go·MySQL-7、MySQL 核心原理全景图谱
开发语言·mysql·golang
acd120092 天前
Go 里 err 被不经意覆盖的几个现场
开发语言·后端·golang
倔强的小石头_3 天前
从Go+SQLite编译到浏览器开局:landlord部署、AI对局与固定地址实战
人工智能·golang·sqlite
tachibana23 天前
Go 语言的 Map 是否是并发安全的?
开发语言·数据结构·后端·golang·go·语言
后除3 天前
Go 实战: 使用 Wails v2.15 构建轻量级的桌面应用 - 仿微信登录界面
开发语言·微信·golang
看浪的路人3 天前
第8讲:RAG 生产化——Go 实现亿级向量召回 + 混合检索 + 重排序 Pipeline
开发语言·后端·golang