锁作用

在 Go 语言中使用 sync.Mutex 进行加锁操作时,​​锁的作用范围是基于锁实例本身,而不是针对特定函数​​。

1 核心概念:锁保护的是数据,而不是代码

2 锁作用于互斥量实例

var mu sync.Mutex // 这是一个锁实例  
func A() {     
    mu.Lock()     
    defer mu.Unlock()     
    // 临界区代码 
    }  
func B() {     
    mu.Lock()     
    defer mu.Unlock()     
    // 另一个临界区代码 
}

在这个例子中:

  • A() 持有了 mu 的锁时

  • 所有试图在 B()A() 或其他函数中锁定同一个 mu 的 goroutine 都会被阻塞

  • 无论这些函数在哪里被调用

锁保护的是代码执行路径,而不是变量本身​​。

var a sync.Mutex  
var b sync.Mutex  
var wg sync.WaitGroup  
var s = 1  
  
func A() {  
    a.Lock()  
    defer a.Unlock()  
    defer wg.Done()  
    s++  
    time.Sleep(1 * time.Second)  
}  
  
func B() {  
    b.Lock()  
    defer b.Unlock()  
    defer wg.Done()  
    s++  
    time.Sleep(1 * time.Second)  
}  
  
func main() {  
    wg.Add(2)  
    go A()  
    go B()  
    wg.Wait()  
    fmt.Println(s)  
}

3 锁使用的基本原则

  1. ​一对一原则​​:每个共享资源应该有自己专用的锁

// 错误:多个资源共用同一个锁
var lockForEverything sync.Mutex

// 正确:每个资源独立锁定
var (
    lockForUsers sync.Mutex
    lockForOrders sync.Mutex
)
  1. ​最小化原则​​:锁住的范围应尽量小

// 错误:长时间操作锁住资源
func process() {
    lock.Lock()
    // 长时间计算...
    // 网络请求...
    lock.Unlock()
}

// 正确:只锁关键操作
func process() {
    // 不锁计算操作...
    result = heavyCalculation()
    
    lock.Lock()
    sharedResource = result // 只锁赋值操作
    lock.Unlock()
}
  1. ​层级原则​​:如果多个资源有关联,用更高层锁保护

type Account struct {
    mu sync.Mutex
    balance int
}

type Bank struct {
    mu sync.Mutex
    accounts map[int]*Account
}

func (b *Bank) Transfer(from, to int, amount int) {
    b.mu.Lock() // 顶级锁保护账户映射
    defer b.mu.Unlock()
    
    fromAcc := b.accounts[from]
    toAcc := b.accounts[to]
    
    fromAcc.mu.Lock()
    defer fromAcc.mu.Unlock()
    toAcc.mu.Lock()
    defer toAcc.mu.Unlock()
    
    fromAcc.balance -= amount
    toAcc.balance += amount
}

锁使用情况

在并发编程中,当多个goroutine(协程)需要访问和修改共享资源时,为了防止数据竞争(data race)和保证数据一致性,我们需要使用锁来同步对共享资源的访问。以下情况需要使用锁:

  1. ​修改共享变量​​:当多个goroutine会同时修改(写入)同一个变量时。

  2. ​读写共享变量​​:当至少有一个goroutine修改共享变量,同时其他goroutine会读取该变量(读操作和写操作同时存在)。

  3. ​操作非原子性的复合结构​​:即使操作看起来是读(如读取一个结构体的多个字段),如果这个读操作期间不允许其他goroutine修改,那么也需要锁(或者使用读写锁中的读锁)。

但是,以下情况不需要锁:

  1. ​只读操作​​:当所有goroutine都只读取共享变量且没有任何写入操作时,因为只读不会造成数据不一致。

  2. ​原子操作​​:某些简单的操作(如整数的自增)可以使用原子操作(sync/atomic包)来避免锁的使用。但是原子操作仅限于简单的整数类型,对于复杂的数据结构还是需要锁。

  3. ​线程局部存储​​:如果每个goroutine操作的是自己的数据(即不共享),则不需要锁。

最后更新于