2 minutes
Select_chan
最近小伙伴问了一个有趣的问题, 如果 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.
....
}
看完上面的代码, 有一个有意思的地方:
- select 的 poll 是随机化的, 所以每次select的时候, 都有每个channel都有数据, 那么就是随机的
- select-case 会对所有的 case 的chan 进行加锁
- 加锁是顺序的, 按照chan地址从小到大, 是大堆排序的, nlog(n)
- select 重复一个 channel, 只会加锁一次, 但是 poll 次数是一样的
- 堵塞在所有chan的时候, 会为每个chan实例化一个sudog挂载在 chan 的 sendq/recv 双链上, 同时, 也会添加到 goroutine 对象的 waiting 单链表上
- 在有chan 通知达到的时候, 会按照goroutine对象的 waiting 单链上的 sudog指针, 从每个chan上的 sendq/recvq 上 进行卸载
值得注意的是, select 的 case 不是按照case的顺序的; 并且根据问题, 参考6, 并不会爆掉, 因此每次select完, 就会从各个chan的 sendq/recvq 上删除