golang sema源码解析

571 阅读4分钟

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)
}

  1. 简单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
    		}
    	}
    }
    
  2. 寻找 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
    }
    
  3. semaRoot 的二叉树中找到自己的位置。并根据 lifo ,添加到对应信号量阻塞的 sudog 的头部或者尾部

  4. 让出当前 goroutine的执行权力,让 p 从新调度其他 goroutine 执行

  5. 被唤醒之后需要和当前正在 semacquire1goroutine竞争获取信号量(参考步骤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()
		}
	}
}
  1. 添加信号量额度
  2. 找到阻塞的 sudog,需要注意出队 sudogticket是0。
  3. 如果 handofftrue,则尝试帮助阻塞的 sudog获取到信号量(这个时候仍然需要和其他尝试 acquiregoroutine竞争)
  4. 修改sudog对应的 goroutine的状态为可运行,并且加到当前prunnext之中。
  5. 如果成功帮助阻塞的sudog获得信号量,并且m没有被lock,则让出执行权给刚释放的sudog对应的 goroutine