Go sync.Mutex源码分析
Go 语言作为一个原生支持用户态进程(Goroutine)的语言,当提到并发编程、多线程编程时,往往都离不开锁这一概念。锁是一种并发编程中的同步原语(Synchronization Primitives),它能保证多个 Goroutine 在访问同一片内存时不会出现竞争条件(Race condition)等问题。
Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once 和 sync.Cond,下面介绍sync.Mutex的具体原理(基于go1.15,不同版本mutex实现不一致)
sync.Mutex是互斥锁,Lock和UnLock
sync.Mutex结构
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
// NOTE: 加起来总共64位,占8字节,在64位机器字长下,可以原子性的操作
// NOTE: 一般而言,机器字长等于寄存器字长,也就是加法器一次能处理的最长数据
// NOTE: 内存/磁盘存储字长于编址相关,一般按字(B)编址
type Mutex struct {
state int32 // 表示当前互斥锁的状态
sema uint32 // 用于控制锁的信号量
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
// Mutex fairness.
// NOTE: 在Go 1.9中引入了fairness优化,防止部分Goroutine饿死
//
// Mutex can be in 2 modes of operations: normal and starvation.
// NOTE: 互斥锁有两种模式: 正常和饥饿模式
// 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.
// NOTE: 正常模式下,waiters出等待队列遵循先入先出的顺序,但并发场景下,新来的goroutine会比刚唤醒的goroutine在抢锁上更有优势
// 因为新来的goroutine已经占据cpu时间片,而刚唤醒的goroutine正在等待调度或切换上下文,锁容易被抢
// 所以当一个waiters超过1ms都没抢到锁,会将锁的模式转换为饥饿模式
//
// 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.
// NOTE: 饥饿模式下,当一个锁释放时,会直接交给等待队列头部的waiters,新来的goroutine在该模式下无法抢锁,也无法自旋,只能被加入
// 到等待队列尾部
//
// 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.
// NOTE: 如果一个waiter获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。
//
// 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.
// NOTE:正常模式相比于饥饿模式有更好的性能
starvationThresholdNs = 1e6 // 1ms
)
互斥锁的状态
- mutexLocked — 表示互斥锁的锁定状态 (1 << 0);
- mutexWoken — 表示当前是否有goroutine被唤醒 (1 << 1);
- mutexStarving — 表示当前的互斥锁是否进入饥饿状态 (1 << 2);
- waitersCount — 当前互斥锁上等待的 Goroutine 个数 (其余29位);
互斥锁加锁操作
// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
// NOTE: 加锁,抢不到就阻塞直到锁可获取,加锁有两种方式
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
// NOTE: Fast path快速加锁方法
// 通过原子性的CAS判断,如果锁的state为0,即互斥锁未被占用,那么将其抢占后直接返回
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
// NOTE: 竞态检测, 用于检测当前是否有其他操作同时操纵此Mutex对象, 没有加上-race时不触发
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
// NOTE: 其他情况都走Slow path模式,尝试通过自旋(Spinnig)等一系列方式等待锁的释放
m.lockSlow()
}
// Slow path lock
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
// Don't spin in starvation mode, ownership is handed off to waiters
// so we won't be able to acquire the mutex anyway.
// NOTE: 在正常模式下,如果锁已经被人占用,并且可以通过自旋检测,那么通过自旋等待互斥锁的释放
// NOTE: 自旋是一种多线程同步机制,当前的进程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。
// 在多核的 CPU 上,自旋可以避免 Goroutine 的切换,使用恰当会对性能带来很大的增益,但是使用的不恰当就会拖慢整个程序
// 所以 Goroutine 进入自旋的条件非常苛刻,条件如下:
// 1. 只有在普通模式下,才允许自旋
// 2. 通过runtime_canSpin检查
// 2.1 运行在多 CPU 的机器上
// 2.2 当前 Goroutine 为了获取该锁进入自旋的次数小于四次
// 2.3 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// Active spinning makes sense.
// Try to set mutexWoken flag to inform Unlock
// to not wake other blocked goroutines.
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
// NOTE: 通过runtime_doSpin进入自旋, 分析见1.1
runtime_doSpin()
iter++
old = m.state
continue
}
// NOTE: 根据上下文,计算当前互斥锁最新状态,相当于构造当前环境下理想的state变量
new := old
// Don't try to acquire starving mutex, new arriving goroutines must queue.
// NOTE: 如果不在饥饿模式,则获取锁,将new的bit 0置为1
if old&mutexStarving == 0 {
new |= mutexLocked
}
// NOTE: 增加当前等待锁的goroutine个数
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// The current goroutine switches mutex to starvation mode.
// But if the mutex is currently unlocked, don't do the switch.
// Unlock expects that starving mutex has waiters, which will not
// be true in this case.
// NOTE: 如果后续判断为饥饿模式,并且锁已经被占用,尝试将最新状态转换为饥饿模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// NOTE: 如果当前waiter处于唤醒状态, 将最新状态的mutexWoken置为1
// 防止唤醒其他等待锁并且此时处于阻塞状态的goroutine,增加无意义的竞争
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
// NOTE: 尝试通过CAS的方式,更新state状态
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// NOTE: 如果上一次的state,锁已经没人占用并且不处于饥饿模式,那么意味着通过CAS更新的state抢到了锁
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// If we were already waiting before, queue at the front of the queue.
// NOTE: 检查是否非第一次等待
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// NOTE: runtime_SemacquireMutex 是runtime提供的用于获取信号量的方法,相当于PV操作的P
// NOTE: 如果没有获取到信号量,当前的goroutine会进入阻塞状态,直到被唤醒
// NOTE: 具体分析见1.2
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// NOTE: 此处是唤醒后执行
// NOTE: 当唤醒后,计算获取锁的时间是否超过阈值,当前是否应该进入饥饿状态
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// NOTE: 在正常模式下,会设置唤醒和饥饿标记、重置迭代次数并重新执行获取锁的循环
// NOTE: 如果已经位于饥饿状态, 当前 Goroutine 会获得互斥锁
// 如果等待队列中只存在当前 Goroutine,互斥锁还会从饥饿模式中退出
if old&mutexStarving != 0 {
// If this goroutine was woken and mutex is in starvation mode,
// ownership was handed off to us but mutex is in somewhat
// inconsistent state: mutexLocked is not set and we are still
// accounted as waiter. Fix that.
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
// Exit starvation mode. 退出饥饿模式
// Critical to do it here and consider wait time.
// Starvation mode is so inefficient, that two goroutines
// can go lock-step infinitely once they switch mutex
// to starvation mode.
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
// 1.1
// NOTE: 一旦当前 Goroutine 能够进入自旋就会调用runtime.procyield 执行 30 次的 PAUSE 指令
// 该指令只会占用 CPU 并消耗 CPU 时间
func sync_runtime_doSpin() {
procyield(active_spin_cnt)
}
TEXT runtime·procyield(SB),NOSPLIT,$0-0
MOVL cycles+0(FP), AX
again:
PAUSE
SUBL $1, AX
JNZ again
RET
// 1.2 sync/runtime.go
// SemacquireMutex is like Semacquire, but for profiling contended Mutexes.
// If lifo is true, queue waiter at the head of wait queue.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_SemacquireMutex's caller.
// NOTE: 如果lifo为true,意味着不是第一次被唤醒,那么会将其插入树堆某个信号量的等待链表中
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)
// 1.3 runtime/sema.go
//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) {
semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes)
}
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) {
gp := getg()
if gp != gp.m.curg {
throw("semacquire not on the G stack")
}
// Easy case.
if cansemacquire(addr) {
return
}
// Harder case:
// increment waiter count
// try cansemacquire one more time, return if succeeded
// enqueue itself as a waiter
// sleep
// (waiter descriptor is dequeued by signaler)
s := acquireSudog()
root := semroot(addr)
t0 := int64(0)
s.releasetime = 0
s.acquiretime = 0
s.ticket = 0
if profile&semaBlockProfile != 0 && blockprofilerate > 0 {
t0 = cputicks()
s.releasetime = -1
}
if profile&semaMutexProfile != 0 && mutexprofilerate > 0 {
if t0 == 0 {
t0 = cputicks()
}
s.acquiretime = t0
}
for {
lockWithRank(&root.lock, lockRankRoot)
// Add ourselves to nwait to disable "easy case" in semrelease.
atomic.Xadd(&root.nwait, 1)
// Check cansemacquire to avoid missed wakeup.
if cansemacquire(addr) {
atomic.Xadd(&root.nwait, -1)
unlock(&root.lock)
break
}
// Any semrelease after the cansemacquire knows we're waiting
// (we set nwait above), so go to sleep.
// NOTE: 入队列
root.queue(addr, s, lifo)
// NOTE: goparkunlock 将当前goroutine修改为阻塞/等待state
goparkunlock(&root.lock, waitReasonSemacquire, traceEvGoBlockSync, 4+skipframes)
if s.ticket != 0 || cansemacquire(addr) {
break
}
}
if s.releasetime > 0 {
blockevent(s.releasetime-t0, 3+skipframes)
}
releaseSudog(s)
}
// Puts the current goroutine into a waiting state and unlocks the lock.
// The goroutine can be made runnable again by calling goready(gp).
func goparkunlock(lock *mutex, reason waitReason, traceEv byte, traceskip int) {
gopark(parkunlock_c, unsafe.Pointer(lock), reason, traceEv, traceskip)
}
// queue adds s to the blocked goroutines in semaRoot.
func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) {
s.g = getg()
s.elem = unsafe.Pointer(addr)
s.next = nil
s.prev = nil
var last *sudog
pt := &root.treap
for t := *pt; t != nil; t = *pt {
if t.elem == unsafe.Pointer(addr) {
// Already have addr in list.
// NOTE: 如果lifo为true,意味着唤醒的goroutine已经等待过,所以入等待队列头
if lifo {
// Substitute s in t's place in treap.
*pt = s
s.ticket = t.ticket
s.acquiretime = t.acquiretime
s.parent = t.parent
s.prev = t.prev
s.next = t.next
if s.prev != nil {
s.prev.parent = s
}
if s.next != nil {
s.next.parent = s
}
// Add t first in s's wait list.
s.waitlink = t
s.waittail = t.waittail
if s.waittail == nil {
s.waittail = t
}
t.parent = nil
t.prev = nil
t.next = nil
t.waittail = nil
} else {
// NOTE: 如果lifo为false,入等待队列尾部
// Add s to end of t's wait list.
if t.waittail == nil {
t.waitlink = s
} else {
t.waittail.waitlink = s
}
t.waittail = s
s.waitlink = nil
}
return
}
last = t
if uintptr(unsafe.Pointer(addr)) < uintptr(t.elem) {
pt = &t.prev
} else {
pt = &t.next
}
}
// Add s as new leaf in tree of unique addrs.
// The balanced tree is a treap using ticket as the random heap priority.
// That is, it is a binary tree ordered according to the elem addresses,
// but then among the space of possible binary trees respecting those
// addresses, it is kept balanced on average by maintaining a heap ordering
// on the ticket: s.ticket <= both s.prev.ticket and s.next.ticket.
// https://en.wikipedia.org/wiki/Treap
// https://faculty.washington.edu/aragon/pubs/rst89.pdf
//
// s.ticket compared with zero in couple of places, therefore set lowest bit.
// It will not affect treap's quality noticeably.
s.ticket = fastrand() | 1
s.parent = last
*pt = s
// Rotate up into tree according to ticket (priority).
for s.parent != nil && s.parent.ticket > s.ticket {
if s.parent.prev == s {
root.rotateRight(s.parent)
} else {
if s.parent.next != s {
panic("semaRoot queue")
}
root.rotateLeft(s.parent)
}
}
}
互斥锁解锁操作
// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// Fast path: drop lock bit.
// NOTE: Fast path模式,直接加上1 << 0,如果返回的新状态等于0,即当前 Goroutine 就成功解锁了互斥锁
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Outlined slow path to allow inlining the fast path.
// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
// 如果新状态不为0,意味着有其他等待的goroutine需要唤醒,开始慢速解锁
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
// NOTE: 先校验锁状态的合法性 — 如果当前互斥锁已经被解锁过了会直接抛出异常 “sync: unlock of unlocked mutex” 中止当前程序
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// NOTE: 正常模式
if new&mutexStarving == 0 {
old := new
for {
// If there are no waiters or a goroutine has already
// been woken or grabbed the lock, no need to wake anyone.
// In starvation mode ownership is directly handed off from unlocking
// goroutine to the next waiter. We are not part of this chain,
// since we did not observe mutexStarving when we unlocked the mutex above.
// So get off the way.
// NOTE: 如果没有等待的gorotine 或者 当前已经有唤醒的goroutine,不操作直接返回
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
// NOTE:减少等待gouroutine数量,同时唤醒一个处于阻塞状态的goroutine
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// NOTE: 饥饿模式
// Starving mode: handoff mutex ownership to the next waiter, and yield
// our time slice so that the next waiter can start to run immediately.
// Note: mutexLocked is not set, the waiter will set it after wakeup.
// But mutex is still considered locked if mutexStarving is set,
// so new coming goroutines won't acquire it.
// NOTE: 饥饿模式下,唤醒等待队列头的waiter, 并将当前锁交给它
runtime_Semrelease(&m.sema, true, 1)
}
}
// sync/runtime.go
// Semrelease atomically increments *s and notifies a waiting goroutine
// if one is blocked in Semacquire.
// It is intended as a simple wakeup primitive for use by the synchronization
// library and should not be used directly.
// If handoff is true, pass count directly to the first waiter.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_Semrelease's caller.
func runtime_Semrelease(s *uint32, handoff bool, skipframes int)
// runtime/sema.go
//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
semrelease1(addr, handoff, skipframes)
}
func semrelease1(addr *uint32, handoff bool, skipframes int) {
root := semroot(addr)
atomic.Xadd(addr, 1)
// Easy case: no waiters?
// This check must happen after the xadd, to avoid a missed wakeup
// (see loop in semacquire).
if atomic.Load(&root.nwait) == 0 {
return
}
// Harder case: search for a waiter and wake it.
lockWithRank(&root.lock, lockRankRoot)
if atomic.Load(&root.nwait) == 0 {
// The count is already consumed by another goroutine,
// so no need to wake up another goroutine.
unlock(&root.lock)
return
}
// NOTE: 从队列头取一个处于阻塞状态的goroutine(*sudo)
s, t0 := root.dequeue(addr)
if s != nil {
atomic.Xadd(&root.nwait, -1)
}
unlock(&root.lock)
if s != nil { // May be slow or even yield, so unlock first
acquiretime := s.acquiretime
if acquiretime != 0 {
mutexevent(t0-acquiretime, 3+skipframes)
}
if s.ticket != 0 {
throw("corrupted semaphore ticket")
}
if handoff && cansemacquire(addr) {
s.ticket = 1
}
// NOTE: 将goroutine唤醒,从阻塞状态转换到就绪态,等待调度
readyWithTime(s, 5+skipframes)
if s.ticket == 1 && getg().m.locks == 0 {
// Direct G handoff
// readyWithTime has added the waiter G as runnext in the
// current P; we now call the scheduler so that we start running
// the waiter G immediately.
// Note that waiter inherits our time slice: this is desirable
// to avoid having a highly contended semaphore hog the P
// indefinitely. goyield is like Gosched, but it emits a
// "preempted" trace event instead and, more importantly, puts
// the current G on the local runq instead of the global one.
// We only do this in the starving regime (handoff=true), as in
// the non-starving case it is possible for a different waiter
// to acquire the semaphore while we are yielding/scheduling,
// and this would be wasteful. We wait instead to enter starving
// regime, and then we start to do direct handoffs of ticket and
// P.
// See issue 33747 for discussion.
// NOTE: handoff的作用是触发goyield, 即饥饿状态相比于普通状态,其goroutine被唤醒后放入的不是公共的就绪队列
// 而是放在P的本地就绪队列中,减少不必要切换的开销
goyield()
}
}
}
func readyWithTime(s *sudog, traceskip int) {
if s.releasetime != 0 {
s.releasetime = cputicks()
}
// NOTE: goready 转换goroutine状态
goready(s.g, traceskip)
}
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
// goyield is like Gosched, but it:
// - emits a GoPreempt trace event instead of a GoSched trace event
// - puts the current G on the runq of the current P instead of the globrunq
func goyield() {
checkTimeouts()
mcall(goyield_m)
}
参考
- https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#rwmutex
- https://blog.nowcoder.net/n/51b9f0ad8fd1474487aa59a7f6cc66b7
- http://legendtkl.com/2016/10/23/golang-mutex/
- https://zhuanlan.zhihu.com/p/350456432
- https://www.jianshu.com/p/79518c4b9bd2?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com
- https://blog.csdn.net/qcrao/article/details/116810643
- http://kmanong.top/kmn/qxw/form/article?id=73092&cate=93