最近小伙伴问了一个有趣的问题, 如果 for-select 监听多个chan, 其中一个是 eventCh, 一个是 closeCh, event 每隔一段时间会有通知, 但是因为 chan blocking 的特性, 每次 slelect 的时候 closeCh 都会将goroutine 添加到 chan 的 sendq, 那么 sendq 的双链 岂不是爆炸?

直观上大家可定认为 不会发生, 会给出一些 猜测的解决方案, 但是到底会发生什么呢? 先写下如下 demo

package main

func main() {

	doCh := make(chan struct{})
	dataCh := make(chan struct{})
	select {
	case <-doCh:
	case <-dataCh:
	}

}

为了方便反汇编, 执行编译: go build -gcflags '-l' -o main main.go

执行反汇编: go tool objdump -s "main" main

  main.go:7		0xdbc			e800000000		CALL 0xdc1				[1:5]R_CALL:runtime.selectgo

其中, 最后可以发现 select 多个case的语句对应 runtime.selectgo, 找到对应的文件实现:

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	if debugSelect {
		print("select: cas0=", cas0, "\n")
	}
	....
	// generate permuted order, 随机化 pollorder 用来select
	norder := 0
	for i := range scases {
		cas := &scases[i]

		// Omit cases without channels from the poll and lock orders.
		if cas.c == nil {
			cas.elem = nil // allow GC
			continue
		}

		j := fastrandn(uint32(norder + 1))
		pollorder[norder] = pollorder[j]
		pollorder[j] = uint16(i)
		norder++
	}
	pollorder = pollorder[:norder]
	lockorder = lockorder[:norder]

	// sort the cases by Hchan address to get the locking order.
	// simple heap sort, to guarantee n log n time and constant stack footprint.
	// 按照 chan 地址 堆排序 select case, 最大堆, 数组(lockorder) 从小到大, 用来有序加锁
	.....

	// lock all the channels involved in the select, 按顺序加锁, 相同的channel不会重复加锁
	sellock(scases, lockorder)

	// pass 1 - look for something already waiting  如果有channel关闭或者可以成功(发送/接受), 处理
	.....
	// pass 2 - enqueue on all chans, 实例化一个sudog, 插入到每个chan的waitq的双链, 同时自己记录, 方便后面通知抵达后从每个chan中删除
	....
	// wait for someone to wake us up
	gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
	....
	// pass 3 - dequeue from unsuccessful chans
	// otherwise they stack up on quiet channels
	// record the successful case, if any.
	// We singly-linked up the SudoGs in lock order.
	....
}

看完上面的代码, 有一个有意思的地方:

  1. select 的 poll 是随机化的, 所以每次select的时候, 都有每个channel都有数据, 那么就是随机的
  2. select-case 会对所有的 case 的chan 进行加锁
  3. 加锁是顺序的, 按照chan地址从小到大, 是大堆排序的, nlog(n)
  4. select 重复一个 channel, 只会加锁一次, 但是 poll 次数是一样的
  5. 堵塞在所有chan的时候, 会为每个chan实例化一个sudog挂载在 chan 的 sendq/recv 双链上, 同时, 也会添加到 goroutine 对象的 waiting 单链表上
  6. 在有chan 通知达到的时候, 会按照goroutine对象的 waiting 单链上的 sudog指针, 从每个chan上的 sendq/recvq 上 进行卸载

值得注意的是, select 的 case 不是按照case的顺序的; 并且根据问题, 参考6, 并不会爆掉, 因此每次select完, 就会从各个chan的 sendq/recvq 上删除