在 sync
包下提供了最基本的同步原语,如互斥锁 Mutex
。除 Once
和 WaitGroup
类型外,大部分是由低级库提供的,更高级别的同步最好是通过 channel
通讯来实现。
Mutex
类型的变量默认值是未加锁状态,在第一次使用后,此值将不得
复制,这点切记!!!
本文基于go version: 1.16.2
Mutex 锁实现了 Locker
接口。
// A Locker represents an object that can be locked and unlocked. type Locker interface { Lock() Unlock() }
锁的模式
为了互斥公平性,Mutex 分为 正常模式
和 饥饿模式
两种。
正常模式
在正常模式下,等待者 waiter
会进入到一个FIFO
队列,在获取锁时waiter
会按照先进先出的顺序获取。当唤醒一个waiter
时它被并不会立即获取锁,而是要与新来的goroutine
竞争,这种情况下新来的goroutine比较有优势,主要是因为它已经运行在CPU,可能它的数量还不少,所以waiter
大概率下获取不到锁。在这种waiter
获取不到锁的情况下,waiter
会被添加到队列的前面。如果waiter
获取不到锁的时间超出了1毫秒,它将被切换为饥饿模式。
这里的 waiter
是指新来一个goroutine 时会尝试一次获取锁,如果获取不到我们就视其为watier
,并将其添加到FIFO队列里。
饥饿模式
在正常模式下,每次新来的goroutine都会抢走锁,就这会导致一些 waiter
永远也获取不到锁,产生饥饿问题。所以为了应对高并发抢锁场景下的公平性,官方引入了饥饿模式。
在饥饿模式下,锁将直接交给队列最前面的waiter
。新来的goroutine即使在锁未被持有情况下也不会参与竞争锁,同时也不会进行自旋,而直接将其添加到队列的尾部。
如果拥有锁的waiter
发现有以下两种情况,它将切换回正常模式:
- 它是队列里的最后一个waiter,再也没有其它waiter
- 等待时间小于1毫秒
模式区别
正常模式
拥有更好的性能,因为即使等待队列里有抢锁的 waiter
,由于新来的goroutine
正在CPU中运行,所以优先获取到锁。饥饿模式
是对公平性和性能的一种平衡,它避免了某些 goroutine
长时间的等待锁。在饥饿模式下,优先处理的是那些一直在等待的 waiter
。饥饿模式在一定机时会切换回正常模式。