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
相关推荐
牛奔10 小时前
Go 如何打印调试深层或嵌套的结构体
开发语言·后端·golang
geovindu11 小时前
go: Iterative Algorithms
开发语言·后端·算法·golang·迭代算法
kite012112 小时前
Go语言Map深度解析与最佳实践
开发语言·后端·golang
Wang's Blog1 天前
Go-Zero 项目开发21: 实现离线消息拉取与会话管理
开发语言·后端·golang
FfHUCisI1 天前
Golang学习-约瑟夫环问题(Josephus Problem)
开发语言·学习·golang
会编程的土豆1 天前
结构体 struct:用代码描述用户、订单、座位
数据库·mysql·golang
llwszx2 天前
【Java/Go后端手撸原生Agent(第九篇):Plan-and-Execute规划模式——从“走一步看一步“到“先谋后动“】
java·python·golang·agent开发·plan模式·规划执行模式
灯澜忆梦2 天前
GO_网络编程---文件传输实战
网络·golang·php
北冥you鱼2 天前
Go 语言新手扫盲:指针 * 和 & 使用场景详解
开发语言·后端·golang