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 的内存模型规定了 Lock 和 Unlock 之间的“同步之前”关系:
- 第
n次调用Unlock()必须在第m次调用Lock()之前发生(对于任何n < m)。 - 成功的
TryLock()调用等效于Lock(),而失败的TryLock()调用则不会建立任何“同步之前”关系。
这意味着,释放锁和获取锁的操作在内存中是有严格的顺序保证的,这样可以确保共享变量在不同 goroutine 之间的内存可见性是正确的。
总结
sync.Mutex是 Go 中用于并发控制的基础工具,它通过互斥机制确保多个 goroutine 在访问共享资源时不会产生数据竞争。Lock()和Unlock()用于控制共享资源的访问,TryLock()则提供了非阻塞的锁定尝试方法。- 在并发环境中使用
Mutex时,必须确保每次加锁都伴随着正确的解锁操作,以避免死锁。