sync.Mutex #
可以看 sync/mutex.go 中的实现,有这样一段注释:
// 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.
对于 golang 中互斥锁的实现,有两种模式:正常模式(normal)和饥饿模式(starvation)。
normal 模式 #
waiter 按照先进先出(FIFO)来排队,当锁释放唤醒队首的 waiter,它不会直接拥有互斥锁,而是和新的 goroutine 争夺互斥锁的所有权。
一般情况下,新的 goroutine 大概率会争夺到互斥锁,因为它已经调度到 cpu 上运行了,而唤醒的 waiter 大概率会失败。在这种情况下,它被放在等待队列的前面重新等待。
当一个 waiter 超过 1ms 仍然没有获取到互斥锁的时候,就会切换到 starvation 模式
starvation 模式 #
在饥饿模式下,互斥锁的所有权直接从解锁的 goroutine 转交给处于等待队列队首的 waiter。新到来的 goroutine 不会尝试获取锁或者自旋,而是直接添加到等待队列的队尾。
当达到以下两个条件是,重新切换回 normal 模式:
- 队尾只有一个 waiter
- waiter 等待的时间少于 1ms
对比 #
normal 模式拥有更好的性能,因为一个 goroutine 可以连续获取多次互斥锁,但是可能会造成队尾的 waiter 长时间抢不到锁。
starvation 模式则可以避免队尾延迟问题。
源码剖析 #
sync.RWMutex #
Go 标准库的 sync/rwmutex.go 实现使用的写优先,即如果一个写操作在等待,那么不管后续有多少个新的读操作,都是写操作优先获取到写锁。但是对于已经获取到了写锁,之后又有新的读锁与写锁在等待的情况,写锁释放后优先唤醒这些已经等待读锁。
注意
- 读写锁不可复制
- 获取与释放需要成对出现,获取了读锁的协程不能再去获取写锁
- 互斥锁不可重入,因此写锁的获取不可重入
- 获取到读锁的协程,不要重入再去获取读锁
In the terminology of the Go memory model,
type RWMutex struct {
// 用于竞争写锁
w Mutex // held if there are pending writers
// 写操作信号量,写操作在该信号量中排队等待、被唤醒
writerSem uint32 // semaphore for writers to wait for completing readers
// 读操作信号量,读操作在该信号量中排队等待、被唤醒
readerSem uint32 // semaphore for readers to wait for completing writers
// 记录当前读操作的数量
// 不管读操作获取到读锁与否,只要调用了 RLock (),都会被计数。
// 该值使用负数的方式来记录当前是否有写操作(写操作已经获取到了互斥锁 w,但是可能还在等读锁释放,也可能已经获取到了写锁)
readerCount int32 // number of pending readers
// 记录写操作请求时已经获取到读锁的读操作数量,即写操作要等待多少个读锁释放。
readerWait int32 // number of departing readers
}
// 支持读锁的最大个数
// 该值会在写操作获取写锁时被用来去减去 readerCount,使 readerCount 变成一个负数,用代表有写操作到来了,进而实现阻塞后续新的读操作获取到读锁
const rwmutexMaxReaders = 1 << 30