Sync包
sync包是go提供的用于并发控制的方法,类似于Java的JUC包。
(图片来自《go设计与实现》)
互斥锁 Mutex
Go 语言的 sync.Mutex
由两个字段 state
和 sema
组成。
state
表示当前互斥锁的状态。sema
是用于控制锁状态的信号量。
type Mutex struct {state int32sema uint32
}
go的互斥锁功能相对比较简单:
mutex 本身没有包含持有这把锁的 goroutine 的信息,因此
- 无法实现可重入锁。
- 可能锁会被其他goroutine释放。
常见的错误场景:
- Lock / UnLock 不是成对出现
- Copy 已使用的 Mutex(不可以复制)
原因:Mutex 是一个有状态的字段,在并发环境下,状态时时在变化。 - 重入
- 死锁
状态
在默认情况下,互斥锁的所有状态位都是 0,int32 中的不同位分别表示了不同的状态:
mutexLocked
— 表示互斥锁的锁定状态。mutexWoken
— 表示从正常模式被从唤醒。mutexStarving
— 当前的互斥锁进入饥饿状态。waitersCount
— 当前互斥锁上等待的 Goroutine 个数。
const (mutexLocked = 1 << iota // mutex is lockedmutexWokenmutexStarvingmutexWaiterShift = iota// Mutex fairness.//// Mutex can be in 2 modes of operations: normal and starvation.// In normal mode waiters are queued in FIFO order, but a woken up waiter// does not own the mutex and competes with new arriving goroutines over// the ownership. New arriving goroutines have an advantage -- they are// already running on CPU and there can be lots of them, so a woken up// waiter has good chances of losing. In such case it is queued at front// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,// it switches mutex to the starvation mode.//// In starvation mode ownership of the mutex is directly handed off from// the unlocking goroutine to the waiter at the front of the queue.// New arriving goroutines don't try to acquire the mutex even if it appears// to be unlocked, and don't try to spin. Instead they queue themselves at// the tail of the wait queue.//// If a waiter receives ownership of the mutex and sees that either// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,// it switches mutex back to normal operation mode.//// Normal mode has considerably better performance as a goroutine can acquire// a mutex several times in a row even if there are blocked waiters.// Starvation mode is important to prevent pathological cases of tail latency.starvationThresholdNs = 1e6
)
sync.Mutex
有两种模式 — 正常模式和饥饿模式。
在正常模式下,锁的等待者会按照先进先出的顺序获取锁,但被唤醒的goroutine不拥有互斥锁,并与新到达的goroutine争夺所有权。
新来的goroutines有一个优势——它们已经在CPU上运行了,而且可能有很多,所以被唤醒的gorountine很有可能会失败。
为了减少这种情况的出现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 饿死。
从性能上来看,如果我们能够把锁交给正在占用 CPU 时间片的 goroutine 的话,那就不需要做上下文的切换,在高并发的情况下,可能会有更好的性能。
饥饿模式是为了保证互斥锁的公平性。
在饥饿模式中,互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。
与饥饿模式相比,正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。