sync锁
一、Mutex
数据结构
type Mutex struct {
state int32 // 表示当前互斥锁的状态
sema uint32 // 用于控制锁状态的信号量
}
state
的基本信息:
-
Locked
互斥锁的锁定状态 -
Woken
表示从正常模式被从唤醒;当一个协程调用 Mutex.Lock() 时,如果 Mutex 已经被其他协程锁定,则该协程将被阻塞,并等待 Mutex 被释放。当其他协程调用 Mutex.Unlock() 释放 Mutex 时,如果等待的协程存在,则会设置 “woken” 标志,以通知等待的协程可以再次尝试获取 Mutex。这样,被阻塞的协程就可以继续执行下去,而不必一直等待下去。
-
Starving
当前的互斥锁进入饥饿状态; -
waitersCount
当前互斥锁上等待的 Goroutine 个数;
模式
- 普通模式
- 锁的等待者会按照先进先出的顺序获取锁。
- 刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁
- 饥饿模式
- 互斥锁会直接交给等待队列最前面的 Goroutine。
- 新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,只会在队列的末尾等待。
- 如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式
加锁 Lock()
源码分析
func (m *Mutex) Lock() {
// 当锁的状态是 0 时,将 mutexLocked 位置成 1
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 不是 0 时 尝试通过自旋(Spinnig)等方式等待锁的释放
m.lockSlow()
}
- 自旋操作的流程
- 判断当前 Goroutine 能否进入自旋;
- 互斥锁只有在普通模式才能进入自旋;
- 运行在多 CPU 的机器上;
- 当前 Goroutine 为了获取该锁进入自旋的次数小于四次;
- 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;
- 通过自旋等待互斥锁的释放;
- 计算互斥锁的最新状态;
- 更新互斥锁的状态并获取锁;
- 判断当前 Goroutine 能否进入自旋;
解锁 UnLock()
源码分析
func (m *Mutex) Unlock() {
// 如果该函数返回的新状态等于 0,当前 Goroutine 就成功解锁了互斥锁;
new := atomic.AddInt32(&m.state, -mutexLocked)
// 不等于 0,开始慢速解锁:
if new != 0 {
m.unlockSlow(new)
}
}
- 慢速解锁的流程
- 正常模式
- 如果互斥锁不存在等待者或者互斥锁的
Locked
、Starving
、Woken
状态不都为 0,直接返回,不需要唤醒其他等待者 - 如果互斥锁存在等待者,会通过
sync.runtime_Semrelease
唤醒等待者并移交锁的所有权;
- 如果互斥锁不存在等待者或者互斥锁的
- 饥饿模式
- 将当前锁交给下一个正在尝试获取锁的等待者
- 等待者被唤醒后会得到锁,在这时互斥锁还不会退出饥饿状态
- 正常模式
总结
- 如果互斥锁处于初始化状态,通过置位
Locked
加锁; - 如果当前 Goroutine 等待锁的时间超过了 1ms,互斥锁就会切换到饥饿模式;
- 当互斥锁已经被解锁时,调用
sync.Mutex.Unlock
会直接抛出异常;
二、RWMutex
数据结构
type RWMutex struct {
w Mutex // 复用互斥锁提供的能力;
writerSem uint32 // 写等待读
readerSem uint32 // 读等待写
readerCount int32 // 存储了当前正在执行的读操作数量;读锁+1 写锁-1
readerWait int32 // 表示当写操作被阻塞时等待的读操作个数;
}
写锁
加锁 sync.RWMutex.Lock
func (rw *RWMutex) Lock() {
rw.w.Lock()
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
- 调用互斥锁,阻塞后续的写操作
- 调研原子操作函数阻塞后续的读操作
- 如果仍有其他Goroutine 持有互斥锁的读锁,该 Goroutine 会调用
runtime.sync_runtime_SemacquireMutex
进入休眠状态等待所有读锁所有者执行结束后释放writerSem
信号量将当前协程唤醒;
解锁 sync.RWMutex.Unlock
func (rw *RWMutex) Unlock() {
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
throw("sync: Unlock of unlocked RWMutex")
}
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
rw.w.Unlock()
}
- 原子操作将
readerCount
变回正数,释放读锁 - 通过 for 循环释放所有因为获取读锁而陷入等待的 Goroutine:
- 释放互斥锁
读锁
加锁 sync.RWMutex.RLock
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
- 原子操作将
readerCount
加一 - 如果原子操作返回的是负数说明其他Goroutine 获得了写锁,这个时候当前 Goroutine 就会调用
runtime.sync_runtime_SemacquireMutex
陷入休眠等待锁的释放
解锁sync.RWMutex.RUnlock
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
rw.rUnlockSlow(r)
}
}
- 原子操作将
readerCount
减一 - 如果返回值
< 0
说明这个时候有症状执行的写操作,这时会调用sync.RWMutex.rUnlockSlow
方法延迟解锁
总结
- 尝试获取写锁时
- 每次
sync.RWMutex.RUnlock
都会将readerCount
其减一,当它归零时该 Goroutine 会获得写锁 - 将
readerCount
减少rwmutexMaxReaders
个数以阻塞后续的读操作;
- 每次
- 释放写锁的时候
- 会先通知所有的读操作,然后才会释放持有的互斥锁;