Golang学习-sync.Mutex 互斥锁
sync.Mutex 互斥锁一、为什么需要互斥锁当多个 goroutine 同时读写同一个变量时会发生数据竞争data race。比如银行账户的例子varbalanceintfuncDeposit(amountint){balancebalanceamount// 这不是原子操作}balance balance amount在 CPU 层面至少拆成三步从内存读取 balance 到寄存器寄存器值加上 amount将结果写回内存两个 goroutine 并发执行 Deposit 时可能出现交错导致某笔存款丢失。这就是数据竞争。二、sync.Mutex 的基本用法Mutex互斥锁保证同一时刻只有一个 goroutine 能进入临界区critical section。var(mu sync.Mutex balanceint)funcDeposit(amountint){mu.Lock()// 获取锁若已被占用则阻塞等待balanceamount// 临界区只有持锁者可以执行mu.Unlock()// 释放锁}约定俗成被 mutex 保护的变量紧跟在 mutex 声明之后。这种编排让阅读者一眼就知道这些变量被这把锁保护。三、defer 释放锁在复杂函数中可能有多个 return 路径人工保证每个路径都 Unlock 很容易遗漏。defer完美解决funcBalance()int{mu.Lock()defermu.Unlock()// 函数退出前自动释放即使发生 panic 也会执行returnbalance}defer 的代价极低远小于它带来的代码安全和可读性收益。永远优先使用 defer 释放锁。四、Go 的 Mutex 不可重入这是 Go 与 Java 的一个重要区别。Java 的 ReentrantLock 允许同一个线程多次获取同一把锁Go 的 sync.Mutex不允许。funcWithdraw(amountint)bool{mu.Lock()defermu.Unlock()Deposit(-amount)// 死锁Deposit 内部又会 Lock 同一个 mu// ...}解决方案将实际逻辑拆分到不导出的内部函数假设锁已被持有funcdeposit(amountint){balanceamount}// 不导出调用者负责持锁funcDeposit(amountint){mu.Lock()defermu.Unlock()deposit(amount)}funcWithdraw(amountint)bool{mu.Lock()defermu.Unlock()deposit(-amount)ifbalance0{deposit(amount)returnfalse}returntrue}这种导出函数加锁内部函数干活的模式在 Go 标准库中很常见。五、Mutex 的零值可用sync.Mutex 的零值就是一个未锁定的锁不需要 New 或 Init。这意味着你可以直接把 Mutex 嵌入结构体声明后就能用。六、练习代码保存为mutex_demo.go编译运行packagemainimport(fmtsynctime)// 银行账户 — 使用 Mutex 保护共享变量typeBankAccountstruct{mu sync.Mutex balanceint64}// 存款导出函数负责加锁func(a*BankAccount)Deposit(amountint64){a.mu.Lock()defera.mu.Unlock()a.deposit(amount)}// 取款func(a*BankAccount)Withdraw(amountint64)bool{a.mu.Lock()defera.mu.Unlock()ifa.balanceamount{returnfalse}a.deposit(-amount)returntrue}// 查询余额func(a*BankAccount)Balance()int64{a.mu.Lock()defera.mu.Unlock()returna.balance}// 内部存款假设调用者已持锁func(a*BankAccount)deposit(amountint64){a.balanceamount}funcmain(){account:BankAccount{}varwg sync.WaitGroup// 模拟 100 个 goroutine 同时存款 1 元fori:0;i100;i{wg.Add(1)gofunc(){deferwg.Done()account.Deposit(1)}()}// 同时进行 50 次取款尝试fori:0;i50;i{wg.Add(1)gofunc(){deferwg.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