golang mutex底层原理
数据结构
位于sync/mutex.go
type Mutex struct {
state int32
sema uint32
}
state
locked:锁的状态,是否已经上锁
woken:表示是否有协程被唤醒,0表示没有协程被唤醒,1表示有协程被唤醒
starving:是否处于饥饿模式
waterCount:等待这把锁的协程数量,同一时刻只会有一个协程拿到锁,其余的都会处于阻塞等待中
sema
信号量,本质上是一个计数器
加锁流程
位于sync/mutex.go
func (m *Mutex) Lock() {
// 先通过一次cas操作加锁,如果成功则获得锁,并返回
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
// ...
return
}
// 否则进入slow path处理逻辑
m.lockSlow()
}
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false // 当前的 goroutine 是否是被唤醒的
iter := 0 // 自旋次数
old := m.state // 先记录一下老的状态
for {
// 如果锁被占用,且是正常模式,并且满足自旋条件,则可以自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 那么利用CAS,将 state 的唤醒标记置为1,标记自己是被唤醒的(在释放锁的时候,就不去等待)
// 如果有其他 goroutine 已经设置了 state 的唤醒标记位,那么本次就会失败
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
// awoke标记为true
awoke = true
}
//自旋,执行没用的指令30次
runtime_doSpin()
iter++
old = m.state
continue
}
// 到这里,说明退出了自旋,当前锁没被占用 或者 系统处于饥饿模式 或者 自旋次数太多导致不符合自旋条件
// ...
// 锁被占用 或者 处于饥饿模式下,新增一个等待者
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// ...
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果改状态之前,锁未被占用 且 处于正常模式,那么就相当于获取到锁了
if old&(mutexLocked|mutexStarving) == 0 {
break
}
// 到这里说明:1. 之前锁被占用 或者 2.之前是处于饥饿状态
// 判断之前是否等待过(是否从队列里唤醒的),之前等待过,再次排队放在队首
queueLifo := waitStartTime != 0
// 如果之前没等过(新来的),设置等待起始时间当前时间
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 之前排过队的老人,放到等待队列队首;新人放到队尾,然后等待获取信号量
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 锁被释放,goroutine 被唤醒
// 设置当前 goroutine 饥饿状态,如果之前已经饥饿,或者距离等待开始时间超过了 1ms,也变饥饿
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 被唤醒后如果是饥饿状态
if old&mutexStarving != 0 {
// ...
// 如果当前队列空了,就把starving清0了,将置为正常模式
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
// 退出循环,获得锁
break
}
// 否则被唤醒后继续尝试自旋和其他协程竞争锁
awoke = true
iter = 0
} else {
old = m.state
}
}
// ...
}
- 先通过atomic()方法尝试将state置为lock状态(本质上就是一个cas操作),如果成功则获取了锁,可以继续执行业务逻辑了。
- 如果没有设置成功,则尝试自旋,自旋的目的是乐观的认为在这段时间内锁被释放了,这样就可以快速地获取锁(如果是饥饿模式下就不执行自旋操作)。
- 如果自旋几次后还没有得到锁,会执行到
SemacquireMutex
,就需要sleep原语来阻塞当前goroutine,并将其放入等待队列(FIFO):如果是新来的goroutine,就需要放在队尾;如果是被唤醒的等待锁的goroutine,就放在队头。之后等待解锁的goroutine发出信号进行唤醒。 - 唤醒之后的goroutine发现锁处于饥饿模式,则能直接拿到锁,否则重置自旋迭代次数并标记唤醒位,重新进入步骤2中
满足自旋的条件
需要同时满足以下几种条件:
1、mutex处于被锁状态
2、正常模式
3、sync_runtime_canSpin
// runtime/proc.go
func sync_runtime_canSpin(i int) bool {
if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
return false
}
if p := getg().m.p.ptr(); !runqempty(p) {
return false
}
return true
}
-
已经自旋的次数小于4次
-
多核
-
最大逻辑处理器大于1
-
并且本地的P队列可运行G队列为空
解锁流程
func (m *Mutex) Unlock() {
// ...
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
fatal("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 { // 如果是正常模式
old := new
for {
// 如果队列中没有等待的goroutine或者有goroutine已经被唤醒并且抢占了锁(这种情况就如lock // 中,正好处在cpu中的goroutine在自旋(自旋状态的goroutine的wake状态为1),那么就不需要 // 再唤醒等待队列了
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 如果队列中有等着的,并且也没有处在cpu中的goroutine去自旋获取锁,
// 那么就抓住机会从等待队列中唤醒一个goroutine
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else { // 如果是饥饿模式
// 直接唤醒队头的goroutine
runtime_Semrelease(&m.sema, true, 1)
}
}
- 如果通过原子操作
AddInt32
后,锁变为完全空闲状态,则直接解锁。 - 如果锁处于正常模式,且没有goroutine等待锁释放,或者锁被其他goroutine设置为了锁定状态、唤醒状态中的任一种(非空闲状态),则会直接退出;否则,会通过wakeup原语
Semrelease
唤醒waiter。 - 如果锁处于饥饿模式,会直接将锁的所有权交给等待队列队头的goroutine,唤醒的goroutine会负责设置
Locked
标志位。
信号量
操作系统的信号量的管理对象是线程,而 Mutex 中使用的信号量是针对协程的,那么这就意味着golang需要重新实现一套基于协程的信号量。实际上sync.Mutex
最终是依赖sema.go
中实现的sleep
和wakeup
原语来实现PV操作的。
数据结构
// src/runtime/sema.go
type semaRoot struct {
lock mutex
treap *sudog // root of balanced tree of unique waiters.
nwait uint32 // Number of waiters. Read w/o the lock.
}
const semTabSize = 251
var semtable [semTabSize]struct {
root semaRoot
pad [cpu.CacheLinePadSize - unsafe.Sizeof(semaRoot{})]byte
}
// src/runtime/runtime2.go
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
acquiretime int64
releasetime int64
ticket uint32
isSelect bool
success bool
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
- 一个samtable有512个semaRoot;
- 一个semarRoot对应一个信号量量的PV操作。
- 一个semaRoot里有一个等待队列sudog,从它的
next
和pre
属性可以得知它支持链表,从它的parent
属性可以得知它也支持树形结构。 channel的读写等待队列就是用的链表,而semaRoot使用的是平衡树。
阻塞
// src/sync/mutex.go
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// src/runtime/sema.go
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()
// ...
// 做一次cas操作来尝试修改信号量的值,如果成功则拿到锁并返回
if cansemacquire(addr) {
return
}
// 构造sudog
s := acquireSudog()
root := semtable.rootFor(addr)
t0 := int64(0)
s.releasetime = 0
s.acquiretime = 0
s.ticket = 0
// ...
for {
lockWithRank(&root.lock, lockRankRoot)
// ...
root.queue(addr, s, lifo) // 将sudog放入等待队列
goparkunlock(&root.lock, waitReasonSemacquire, traceEvGoBlockSync, 4+skipframes)
if s.ticket != 0 || cansemacquire(addr) {
break
}
}
// ...
releaseSudog(s)
}
func lockWithRank(l *mutex, rank lockRank) {
lock2(l)
}
func lock2(l *mutex) {
gp := getg()
// ...
gp.m.locks++
for {
// ...
futexsleep(key32(&l.key), mutex_sleeping, -1)
}
}
- 做一次cas操作来尝试修改信号量的值,如果成功则拿到锁并返回;
- 构造封装当前 goroutine 的 sudog 对象;
- 调用futexsleep()进入休眠,其最终发起操作系统底层的futex()调用;
- 将sudog放入等待队列;
- gopark 当前协程,进入gwaiting状态;
唤醒
有阻塞,就需要有对应的唤醒机制
// src/sync/mutex.go
runtime_Semrelease(&m.sema, false, 1)
// src/runtime/sema.go
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
semrelease1(addr, handoff, skipframes)
}
func semrelease1(addr *uint32, handoff bool, skipframes int) {
root := semtable.rootFor(addr)
// ...
s, t0 := root.dequeue(addr)
// ...
unlock(&root.lock)
if s != nil {
readyWithTime(s, 5+skipframes)
// ...
if s.ticket == 1 && getg().m.locks == 0 {
goyield()
}
}
}
func unlock(l *mutex) {
unlockWithRank(l)
}
func unlockWithRank(l *mutex) {
unlock2(l)
}
func unlock2(l *mutex) {
v := atomic.Xchg(key32(&l.key), mutex_unlocked)
// ...
if v == mutex_sleeping {
futexwakeup(key32(&l.key), 1) // 唤醒阻塞的goroutine
}
// ...
}
func readyWithTime(s *sudog, traceskip int) {
if s.releasetime != 0 {
s.releasetime = cputicks()
}
goready(s.g, traceskip)
}
- 从队列取出goroutine;
- 调用futexwakeup()解除阻塞,其最终发起操作系统底层的futex()调用;
- 执行goready(),将goroutine加入到本地P队列。