sync.Mutex / RWMute原理
sync.Mutex / RWMute原理
sync.Mutex
Go 的 sync.Mutex 底层主要由 state + semaphore 两个字段组成。
1
2
3
4
type Mutex struct {
state int32 // 状态
sema uint32 // 信号量
}
state位:
- waiter_num:等待goroutine
- starving:是否处于饥饿状态
- woken:是否有goroutine正在加锁
- locked:是否有goroutine持有该锁
Mutex.sema
- 信号量,用于唤醒阻塞的goroutine
加锁时首先通过 CAS(Compare and Swap,比较并交换) 尝试抢锁,如果失败会进行 短暂自旋,自旋仍失败则通过 信号量机制将 goroutine 挂起并加入等待队列。
解锁时会修改 state 状态,并通过信号量机制唤醒等待的 goroutine。
锁的两种模式
1
2
引入饥饿模式,保证锁公平性
公平性:goroutine获取锁的顺序与请求锁的顺序一致
- 正常模式
- 唤醒阻塞队列队头的goroutine,该goroutine与新请求锁的goroutine共同竞争锁,新请求的goroutine大概率会获得锁,因为占有cpu时间片
- 饥饿模式
- 唤醒阻塞队列队头的goroutine直接获得锁
- 新请求锁goroutine不参与锁竞争
1 2 3 4 5
饥饿模式的触发条件: 1、有一个goroutine获取锁的时间超过1ms 饥饿模式的取消条件: 1、获取到锁的goroutine为阻塞队列的最后一个时,恢复正常模式 2、获取到锁的goroutine等待时间小于1ms,恢复正常模式
sync.RWMute
1
2
3
4
5
6
7
type RWMutex struct {
w Mutex // 写锁(本质还是 Mutex)
readerCount int32 // 当前读者数量
readerWait int32 // 等待释放的读者数
writerSem uint32 // 写等待信号量
readerSem uint32 // 读等待信号量
}
RWMute是通过Mutex + 计数器 + 读/写信号量实现的.
如何解决写饥饿问题?
- 抢占写锁 -> 阻止新读锁加锁 -> 等待已有读锁结束
什么情况下使用RWMute,什么情况下使用Mutex
- RWMute:适用读多写少场景。
- Mutex:适用低并发,读写平均场景。
因为RWMute频繁加写锁开销比Mutex大,RWMute的写锁需要解决写饥饿。
Panic场景
注意:重复加锁不是Panic,是死锁
- 解锁未加锁的 Mutex
1 2 3 4
func main() { var mu sync.Mutex mu.Unlock() // fatal error: sync: unlock of unlocked mutex } - 重复解锁
1 2 3 4 5 6
func main() { var mu sync.Mutex mu.Lock() mu.Unlock() mu.Unlock() // panic: sync: unlock of unlocked mutex }
- RWMutex 未加读/写锁就解
1 2 3 4 5 6 7
func main() { // 场景 A:读锁未加就解 rw.RUnlock() // panic: sync: RUnlock of unlocked RWMutex // 场景 B:写锁未加就解 rw.Unlock() // panic: sync: unlock of unlocked mutex }
This post is licensed under CC BY 4.0 by the author.
