Go语言互斥锁(mutex)

在并发编程中,当多个goroutine同时访问共享资源时,可能会发生数据竞态(data race)。互斥锁(mutual exclusion lock)是保护共享资源最常用的同步原语。Go语言的sync.Mutex提供了简单的Lock()和Unlock()方法,保证同一时刻只有一个goroutine可以执行被保护的代码段。此外,sync.RWMutex实现了读写锁,适用于读多写少的场景。

设计原则:“不要通过共享内存来通信,而要通过通信来共享内存” —— 但在某些场景(如保护内部状态),互斥锁依然是简洁有效的工具。

1. 没有互斥锁:数据竞态示例

多个goroutine不加保护地修改共享变量会导致结果不正确。

有竞态的计数器
var counter int
var wg sync.WaitGroup

func increment() {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        counter++  // 非原子操作,存在竞态
    }
}

func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go increment()
    }
    wg.Wait()
    fmt.Println("最终计数:", counter) // 通常小于10000,且每次结果不同
}
使用go run -race main.go可以检测数据竞态。

2. sync.Mutex 基本用法

Lock()获取锁,Unlock()释放锁。通常配合defer确保锁被释放。

加锁保护计数器
import "sync"

var mu sync.Mutex
var counter int

func incrementSafe() {
    for i := 0; i < 1000; i++ {
        mu.Lock()
        counter++
        mu.Unlock()
    }
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            incrementSafe()
        }()
    }
    wg.Wait()
    fmt.Println("最终计数:", counter) // 总是10000
}
注意:锁的粒度要适中,避免长时间持有锁导致性能下降。

3. 读写锁(RWMutex)

sync.RWMutex区分读锁和写锁:读锁可以并发持有,写锁独占。适用于读多写少的场景。

type SafeMap struct {
    mu sync.RWMutex
    data map[string]int
}

func (m *SafeMap) Get(key string) int {
    m.mu.RLock()         // 读锁
    defer m.mu.RUnlock()
    return m.data[key]
}

func (m *SafeMap) Set(key string, value int) {
    m.mu.Lock()          // 写锁
    defer m.mu.Unlock()
    m.data[key] = value
}

func (m *SafeMap) Delete(key string) {
    m.mu.Lock()
    defer m.mu.Unlock()
    delete(m.data, key)
}

读锁之间不互斥,可提高并发读的性能。

4. 死锁预防

死锁常见情况:同一goroutine重复加锁、多个锁互相等待。

// 错误:在解锁前再次加锁(单线程死锁)
mu.Lock()
mu.Lock()   // 死锁!同一个goroutine不能再次获取已持有的锁

// 正确做法
mu.Lock()
// ... 操作
mu.Unlock()
// 如果需要再次加锁,在解锁之后

避免死锁的方法:按相同顺序获取锁,使用defer确保解锁,避免嵌套锁复杂度过高。

5. 原子操作(atomic包)

对于整型、指针等简单类型,可以使用sync/atomic包进行无锁原子操作,性能优于互斥锁。

import "sync/atomic"

var counter int64

func incrementAtomic() {
    atomic.AddInt64(&counter, 1)
}
func main() {
    // 多个goroutine调用 incrementAtomic 安全,无需互斥锁
}

6. 不要复制锁

sync.Mutex和sync.RWMutex不允许值拷贝,否则会导致锁失效或死锁。应使用指针传递或封装在结构体中。

// 错误:值传递会复制锁
func (m MyMutex) Lock() { // 接收者为值,锁被复制
    m.Mutex.Lock()
}

// 正确:使用指针接收者
func (m *MyMutex) Lock() {
    m.Mutex.Lock()
}

7. 互斥锁与通道的选择

Go官方建议:当共享资源需要被多个goroutine访问并保护时,使用互斥锁;当需要在goroutine之间传递数据所有权或协调行为时,使用通道。两者并非互斥,有时可以结合使用。

综合示例:银行账户转账

使用互斥锁保护账户余额
package main

import (
    "fmt"
    "sync"
)

type Account struct {
    balance int
    mu      sync.Mutex
}

func (a *Account) Deposit(amount int) {
    a.mu.Lock()
    defer a.mu.Unlock()
    a.balance += amount
}

func (a *Account) Withdraw(amount int) bool {
    a.mu.Lock()
    defer a.mu.Unlock()
    if a.balance < amount {
        return false
    }
    a.balance -= amount
    return true
}

func (a *Account) Balance() int {
    a.mu.Lock()
    defer a.mu.Unlock()
    return a.balance
}

// 转账:避免死锁,按账户ID顺序加锁
func Transfer(from, to *Account, amount int) {
    // 简单起见,假设这里使用锁顺序:按地址大小排序
    if from == to {
        return
    }
    // 按地址顺序加锁,避免死锁
    var first, second *sync.Mutex
    if from < to {
        first, second = &from.mu, &to.mu
    } else {
        first, second = &to.mu, &from.mu
    }
    first.Lock()
    second.Lock()
    defer first.Unlock()
    defer second.Unlock()

    if from.balance >= amount {
        from.balance -= amount
        to.balance += amount
    }
}

func main() {
    alice := &Account{balance: 1000}
    bob := &Account{balance: 500}
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            Transfer(alice, bob, 100)
        }()
    }
    wg.Wait()
    fmt.Printf("Alice余额: %d, Bob余额: %d\n", alice.Balance(), bob.Balance())
}
Alice余额: 0, Bob余额: 1500(或类似,取决于转账顺序)
小结:互斥锁是并发编程中最常用的同步工具,正确使用可以避免数据竞态。读写锁优化读多写少场景;对于简单的计数器,原子操作更轻量。始终注意锁的粒度、复制问题以及潜在的死锁。下一章将学习Go语言中的并发模式(如Pipeline、Worker Pool等)。