Go中的互斥锁sync.Mutex理解

61 阅读3分钟

Mutex


// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
//
// In the terminology of [the Go memory model],
// the n'th call to [Mutex.Unlock] “synchronizes before” the m'th call to [Mutex.Lock]
// for any n < m.
// A successful call to [Mutex.TryLock] is equivalent to a call to Lock.
// A failed call to TryLock does not establish any “synchronizes before”
// relation at all.
//
// [the Go memory model]: <https://go.dev/ref/mem>
type Mutex struct {
	state int32
	sema  uint32
}

sync.Mutex 是 Go 标准库中提供的一种用于互斥锁的结构体,主要用于在并发程序中确保只有一个 goroutine 在同一时间访问共享资源,从而避免数据竞争(data race)。

以下是对 sync.Mutex 的详细讲解:

1. 互斥锁(Mutex)的作用

Mutex 是一种互斥锁,其主要作用是用来保护共享资源的访问,确保在任意时刻只有一个 goroutine 能够访问某个关键代码区段,其他 goroutine 必须等待锁被释放后才能进入。

例如,当多个 goroutine 同时修改一个共享变量时,可能会导致数据不一致。使用互斥锁可以确保只有一个 goroutine 能修改该变量,其他 goroutine 会被阻塞,直到锁被释放。

2. 零值锁

sync.Mutex 的零值是一个未上锁的互斥锁,也就是说,直接声明一个 Mutex 变量(例如 var mu sync.Mutex)后,可以立即使用而无需初始化。Go 提供的很多类型都具有这样的“零值可用”特性。

var mu sync.Mutex

3. 主要方法

  • mu.Lock():用于锁定互斥锁。如果锁已经被其他 goroutine 锁定,当前 goroutine 会被阻塞,直到获取到锁为止。
  • mu.Unlock():用于解锁互斥锁。如果有其他 goroutine 因等待锁而被阻塞,当锁被解锁时,其中一个 goroutine 会获得锁并继续执行。
  • mu.TryLock()(Go 1.18 引入):尝试锁定互斥锁,如果锁已经被占用,TryLock 会立即返回 false,而不会阻塞等待。如果锁成功获得,返回 true

4. Mutex 的内部实现

sync.Mutex 的内部定义如下:

type Mutex struct {
	state int32  // 表示当前锁的状态(是否被锁定)
	sema  uint32 // 用于协作 goroutine 之间的调度
}
  • state:这个字段表示锁的当前状态。通常是一个整数,用来标识锁是处于锁定状态还是未锁定状态。
  • sema:这是一个用于实现 goroutine 调度的信号量。当 goroutine 尝试获取一个已经被锁定的锁时,它会被阻塞并放入等待队列。当锁被释放时,信号量将会唤醒等待中的 goroutine。

5. 注意事项

  • 不能复制 Mutex:在 Mutex 第一次使用后,不能将它复制到其他地方。复制 Mutex 会导致无法预期的行为,因为复制后的锁和原锁的状态并不共享。

    mu := sync.Mutex{}
    mu2 := mu  // 错误操作:不能复制 Mutex
    
  • Lock/Unlock 必须配对:每一次调用 Lock(),必须要有对应的 Unlock()。如果锁定后忘记解锁,可能会导致程序死锁,即其他 goroutine 永远无法获取该锁。

6. Go 内存模型中的“synchronizes before”

Go 的内存模型规定了 LockUnlock 之间的“同步之前”关系:

  • n 次调用 Unlock() 必须在第 m 次调用 Lock() 之前发生(对于任何 n < m)。
  • 成功的 TryLock() 调用等效于 Lock(),而失败的 TryLock() 调用则不会建立任何“同步之前”关系。

这意味着,释放锁和获取锁的操作在内存中是有严格的顺序保证的,这样可以确保共享变量在不同 goroutine 之间的内存可见性是正确的。

总结

  • sync.Mutex 是 Go 中用于并发控制的基础工具,它通过互斥机制确保多个 goroutine 在访问共享资源时不会产生数据竞争。
  • Lock()Unlock() 用于控制共享资源的访问,TryLock() 则提供了非阻塞的锁定尝试方法。
  • 在并发环境中使用 Mutex 时,必须确保每次加锁都伴随着正确的解锁操作,以避免死锁。