acquire
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {
gp := getg()
if gp != gp.m.curg {
throw("semacquire not on the G stack")
}
// 1. 如果有余额,直接获取
// 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()
// 2. 找到根节点
root := semtable.rootFor(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.
root.nwait.Add(1)
// Check cansemacquire to avoid missed wakeup.
if cansemacquire(addr) {
root.nwait.Add(-1)
unlock(&root.lock)
break
}
// 3. 找到addr在semaRoot中的位置,并把当前sudog插入到列表中
// Any semrelease after the cansemacquire knows we're waiting
// (we set nwait above), so go to sleep.
root.queue(addr, s, lifo)
// 4. park当前goroutine,等待唤醒
goparkunlock(&root.lock, reason, traceEvGoBlockSync, 4+skipframes)
// 5. 被唤醒之后
if s.ticket != 0 || cansemacquire(addr) {
break
}
}
if s.releasetime > 0 {
blockevent(s.releasetime-t0, 3+skipframes)
}
releaseSudog(s)
}
-
简单case
cansemacquire。通过cas来获取信号量,如果为0则说明没有信号量,需要后续通过复杂方式来获得信号量。如果cas成功则说明成功获得信号量,acqure步骤可以到此结束。func cansemacquire(addr *uint32) bool { for { v := atomic.Load(addr) if v == 0 { return false } if atomic.Cas(addr, v, v-1) { return true } } } -
寻找
semaRoot。在golang中有个全局的semaTable变量用来保存所有获取信号量的时候阻塞的sudog。semaTable是一个长度为251的数组,addr按照251取余可以得到对应的根节点(每个semaRoot的修改都需要锁,通过251个元素来避免对锁的竞争过大)。每个semaRoot有一个属性treap用来保存等待获取信号量的sudog,是一个具有堆性质的二叉搜索树,通过随机ticket,让二叉树不退化成链表。treap在ticket的角度来看是小顶堆,在addr角度来看是一个二叉搜索树。type semaRoot struct { lock mutex treap *sudog // root of balanced tree of unique waiters. nwait atomic.Uint32 // Number of waiters. Read w/o the lock. } var semtable semTable // Prime to not correlate with any user patterns. const semTabSize = 251 type semTable [semTabSize]struct { root semaRoot pad [cpu.CacheLinePadSize - unsafe.Sizeof(semaRoot{})]byte } func (t *semTable) rootFor(addr *uint32) *semaRoot { return &t[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root } -
在
semaRoot的二叉树中找到自己的位置。并根据lifo,添加到对应信号量阻塞的sudog的头部或者尾部 -
让出当前
goroutine的执行权力,让p从新调度其他goroutine执行 -
被唤醒之后需要和当前正在
semacquire1的goroutine竞争获取信号量(参考步骤1),当前被唤醒的goroutine可能会失败,所以需要for循环。
release
func semrelease1(addr *uint32, handoff bool, skipframes int) {
root := semtable.rootFor(addr)
// 1. 添加信号量额度
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 root.nwait.Load() == 0 {
return
}
// 2. 找到阻塞sudog
// Harder case: search for a waiter and wake it.
lockWithRank(&root.lock, lockRankRoot)
if root.nwait.Load() == 0 {
// The count is already consumed by another goroutine,
// so no need to wake up another goroutine.
unlock(&root.lock)
return
}
s, t0 := root.dequeue(addr)
if s != nil {
root.nwait.Add(-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")
}
// 3. 在交出执行权,并且可以获得信号量的时候,ticket设置成1
if handoff && cansemacquire(addr) {
s.ticket = 1
}
// 4. 将sudog对应的goroutine当前p中的下一个goroutine里
readyWithTime(s, 5+skipframes)
// 5. 可以正常让出执行权,并且m没有被
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.
goyield()
}
}
}
- 添加信号量额度
- 找到阻塞的
sudog,需要注意出队sudog的ticket是0。 - 如果
handoff是true,则尝试帮助阻塞的sudog获取到信号量(这个时候仍然需要和其他尝试acquire的goroutine竞争) - 修改
sudog对应的goroutine的状态为可运行,并且加到当前p的runnext之中。 - 如果成功帮助阻塞的
sudog获得信号量,并且m没有被lock,则让出执行权给刚释放的sudog对应的goroutine