select 源码
前言
本文是 Go 运行时 runtime/select.go 中 selectgo 及相关辅助逻辑 的阅读笔记:从编译器与 runtime 约定的 scase 布局写起,顺着 pollorder / lockorder 的构造、sellock 持锁后的快路径与阻塞路径、sudog 入队与 gopark 唤醒后的收尾,把多路 channel 选择拆成可对照源码的步骤;阅读前若对 select 语句本身尚不熟悉,可先看过 [select] 中的概念与示意图。
case 与 scase
// 编译器与 runtime 约定:每个 case 对应一个 scase,记录 channel 与元素指针。
type scase struct {
c *hchan // 参与本 case 的 channel,nil 表示该路被编译器/反射置空
elem unsafe.Pointer // 发送源或接收目标地址
}
以下为 runtime/select.go 摘录。
// 空 select {}:无任何 case,当前 G 永久 gopark(与 channel 无关)。
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceBlockForever, 1) // forever
}
// 按 lockorder 顺序对涉及的 channel 加锁;相邻重复 channel 只加一次。
func sellock(scases []scase, lockorder []uint16) {
var c *hchan
for _, o := range lockorder {
c0 := scases[o].c
if c0 != c {
c = c0
lock(&c.lock)
}
}
}
// 与 sellock 逆序解锁;同一 channel 连续出现时只解最后一次对应的锁。
func selunlock(scases []scase, lockorder []uint16) {
for i := len(lockorder) - 1; i >= 0; i-- {
c := scases[lockorder[i]].c
if i > 0 && c == scases[lockorder[i-1]].c {
continue
}
unlock(&c.lock)
}
}
// gopark 在 select 里用的 commit:按 gp.waiting 链表顺序解开各 channel 的锁,再让 G 休眠。
func selparkcommit(gp *g, _ unsafe.Pointer) bool {
// ... 按 gp.waiting 解锁各 channel(与 gopark 配对),见 select.go 63–100 行
return true
}
selectgo 总览
- 对每个非 nil case 用随机插入构造 pollorder,再按各 channel 地址堆排序得到 lockorder,然后 sellock 锁住本次 select 涉及的全部 channel。
- 第一轮按 pollorder 看是否已有 case 可立即执行:接收看 sendq、buf、是否已关闭;发送看是否已关闭、recvq、buf 是否有空位。能则完成收发或缓冲区读写,selunlock,返回选中下标与 recvOK。recvOK 只对接收有意义(例如从已关闭且无数据的 channel 读会得到 false)。
- 若 block 为 false(有 default)且第一轮无人能执行:selunlock,返回下标 -1,表示走 default。
- 若 block 为 true(无 default):为每个 case 建 sudog,按收发挂到 sendq 或 recvq,gopark;被唤醒后按 lockorder 从各队列摘掉未选中的 sudog,完成中奖 case,selunlock,返回下标与 recvOK。
入口与切片布局
函数参数:
- cas0:指向栈上的 [ncases]scase(ncases = nsends + nrecvs,且 ncases ≤ 65536)。
- 每个元素对应一个非 default 的收发 case;编译器已填好 c、elem。
- selectgo 从中选一个 case 执行并返回其下标。
- order0:指向栈上的 [2*ncases]uint16 连续缓冲区(一整块内存,不是两个独立数组)。编译器不会为它初始化内容;真正写入顺序的是 selectgo 内部逻辑:
- 前半段(长度 ncases)当作 pollorder;
- 后半段(长度 ncases)当作 lockorder(数组里存的是 scase 下标,不是 channel 指针;排序依据是各 case 对应 *hchan 的地址,保证全序加锁)。
- pc0:仅在 -race 构建下指向栈上 [ncases]uintptr,记录各 case 的调用 PC,供 race 标注;非 race 构建为 nil。
- nsends、nrecvs:scases 里前 nsends 个为发送,后 nrecvs 个为接收(编译器生成时的约定顺序)。
- block:false 表示有 default(可以非阻塞返回);true 表示没有 default,第一轮无人可执行时必须阻塞。
函数返回值:
- int:选中的 scase 下标(与源码里 case 顺序一致;选 default 时为 -1)。
- bool(recvOK):仅当选中的是接收 case 时有意义——是否真正收到一个值(例如从已关闭且无缓冲数据的 channel 读时为 false)。发送 case 不关心此返回值。
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
gp := getg()
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
ncases := nsends + nrecvs
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases] // 指向前 ncases 个 uint16:随机轮询顺序(存 case 下标)
lockorder := order1[ncases:][:ncases:ncases] // 指向后 ncases 个 uint16:加锁顺序(存 case 下标)
Go 里除了常见的 s[low:high],还可以写 s[low:high:max]
- pollorder := order1[:ncases:ncases]
- 长度 len = ncases(最多能装 ncases 个 uint16);容量 cap 也 = ncases
- lockorder := order1[ncases:][:ncases:ncases]
- 先 order1[ncases:]:从下标 ncases 切到末尾,即指向「后半段」起点。
- 再 [:ncases:ncases]:在这后半段里再取前 ncases 个元素,容量也是 ncases。
- 效果:lockorder 只覆盖「后半段」这 ncases 个槽。
pollorder:随机插入
对每个非 nil case 用随机插入构造 pollorder,避免总是优先检查固定下标的 case(公平性)。norder 同时用于截断 pollorder、lockorder 的有效长度。
norder := 0
for i := range scases {
cas := &scases[i]
if cas.c == nil {
cas.elem = nil
continue
}
// ... synctest、timer,略
j := cheaprandn(uint32(norder + 1)) // j 在 [0, norder] 上均匀随机
pollorder[norder] = pollorder[j]
pollorder[j] = uint16(i)
norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]
最终有效排列长度是 norder
lockorder:按 channel 地址排序
对 lockorder 按各 Hchan 地址做堆排序,保证全序加锁,避免死锁。算法分两段,都是经典堆操作
for i := range lockorder {
j := i
// Start with the pollorder to permute cases on the same channel.
c := scases[pollorder[i]].c
for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
k := (j - 1) / 2
lockorder[j] = lockorder[k]
j = k
}
lockorder[j] = pollorder[i]
}
for i := len(lockorder) - 1; i >= 0; i-- {
o := lockorder[i]
c := scases[o].c
lockorder[i] = lockorder[0]
j := 0
for {
k := j*2 + 1
if k >= i {
break
}
if k+1 < i && scases[lockorder[k]].c.sortkey() < scases[lockorder[k+1]].c.sortkey() {
k++
}
if c.sortkey() < scases[lockorder[k]].c.sortkey() {
lockorder[j] = lockorder[k]
j = k
continue
}
break
}
lockorder[j] = o
}
sellock 与局部变量
waitReason 一般为 waitReasonSelect(synctest 等场景可替换)。sellock 之后才能在持锁状态下走快路径与阻塞路径。下面一组变量贯穿 pass2 唤醒与各个 goto 标签。
// ... 按 Hchan 地址堆排序 lockorder(两处 for),见上文「lockorder」
waitReason := waitReasonSelect
// ... synctest 时可为 waitReasonSynctestSelect,略
sellock(scases, lockorder) // 上文已有,已持锁后再做快路径与阻塞路径
var (
sg *sudog
c *hchan
cas *scase
casi int
caseSuccess bool // 唤醒后:接收是否收到值 / 发送是否成功(关闭时为 false)
caseReleaseTime int64 = -1
recvOK bool // 最终返回:对接收 case 表示是否收到有效值
k *scase
sglist *sudog
sgnext *sudog
qp unsafe.Pointer
nextp **sudog
)
pass 1:按 pollorder 快路径
语义与单独 chansend、chanrecv 快路径一致:接收优先与 sendq 交接、再读 buf、再处理关闭;发送先判关闭、再与 recvq 交接、再写 buf。
// pass 1:按 pollorder 尝试立即完成,语义与单独 chansend/chanrecv 快路径一致。
for _, casei := range pollorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
if casi >= nsends {
// 接收:先等有无人发送;再 buf;再关闭
sg = c.sendq.dequeue()
if sg != nil {
goto recv // 与等待中的发送者直接交接
}
if c.qcount > 0 {
goto bufrecv // 从环形 buf 读
}
if c.closed != 0 {
goto rclose // 已关闭且无数据可读
}
} else {
// 发送:先判关闭;再等接收者;再 buf
if raceenabled {
racereadpc(c.raceaddr(), casePC(casi), chansendpc)
}
if c.closed != 0 {
goto sclose // 往已关闭 channel 发 → panic
}
sg = c.recvq.dequeue()
if sg != nil {
goto send // 与等待中的接收者交接
}
if c.qcount < c.dataqsiz {
goto bufsend // 写入环形 buf
}
}
}
分支在后文
有 default:直接返回 -1
有 default 且第一轮无人能执行时不阻塞,下标 -1 表示走 default。
if !block {
selunlock(scases, lockorder)
casi = -1
goto retc
}
pass 2:入队与 gopark
无 default 时,每个 case 挂一个 sudog,可同时排在多个 channel 的等待队列上;gopark 时由 selparkcommit 释放 channel 锁。
// pass 2:无 default 时,每个 case 挂一个 sudog,同时排在多个 channel 的等待队列上。
nextp = &gp.waiting
for _, casei := range lockorder {
...
sg := acquireSudog()
...
if casi < nsends {
c.sendq.enqueue(sg)
} else {
c.recvq.enqueue(sg)
}
if c.timer != nil {
blockTimerChan(c)
}
}
gp.param = nil
gp.parkingOnChan.Store(true)
gopark(selparkcommit, nil, waitReason, traceBlockSelect, 1) // selparkcommit 内会释放 channel 锁
gp.activeStackChans = false
gopark+selparkcommit
- 调用 gopark 时,当前还握着这次 select 在 sellock 里加上的那些 channel 锁。若带着锁去睡,别人就没法改 channel 状态,会死锁。
- 所以 gopark 的 commit 回调用的是 selparkcommit:在 G 真正挂起前,沿着 gp.waiting 里 sudog 的顺序,按约定顺序把相关 channel 的锁都解开。这样别的 M 上的 goroutine 才能对同一批 channel 做收发,从而在将来唤醒你。
- 上文有 selparkcommit 函数
唤醒:重新加锁与取出中奖 sudog
唤醒者把中奖的 sudog 指针放在 gp.param。
sellock(scases, lockorder) // 被唤醒后重新加锁
gp.selectDone.Store(0)
sg = (*sudog)(gp.param) // 唤醒者把中奖的 sudog 指针放在 gp.param
gp.param = nil
pass 3:沿 lockorder 摘掉未中奖 sudog
与 gp.param 匹配的那路即为中奖 case;其余 channel 上从对应队列 dequeueSudoG 摘掉并 releaseSudog。若选中发送且 channel 在等待期间关闭,caseSuccess 为 false,后续走 sclose panic。
// pass 3:沿 lockorder 摘掉未选中的 sudog;与 gp.param 匹配的那路即为中奖 case。
casi = -1
cas = nil
caseSuccess = false
sglist = gp.waiting
// ... 遍历 gp.waiting,清除各 sudog 的 isSelect/elem/c;gp.waiting = nil
for _, casei := range lockorder {
k = &scases[casei]
if k.c.timer != nil {
unblockTimerChan(k.c)
}
if sg == sglist {
casi = int(casei)
cas = k
caseSuccess = sglist.success
if sglist.releasetime > 0 {
caseReleaseTime = sglist.releasetime
}
} else {
c = k.c
if int(casei) < nsends {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}
if cas == nil {
throw("selectgo: bad wakeup")
}
c = cas.c
if casi < nsends {
if !caseSuccess {
goto sclose
}
} else {
recvOK = caseSuccess // 接收:recvOK 即「是否收到值」
}
...
selunlock(scases, lockorder)
goto retc
标签 bufrecv / bufsend:环形缓冲区
快路径命中缓冲区时,从 recvx、sendx 读写环形 buf,更新 qcount 后解锁返回。
bufrecv:
// ... race/msan/asan,见 446–459 行
recvOK = true
qp = chanbuf(c, c.recvx) // 从环形缓冲区头部取一槽
if cas.elem != nil {
typedmemmove(c.elemtype, cas.elem, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
selunlock(scases, lockorder)
goto retc
bufsend:
// ... race/msan/asan,见 476–485 行
typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem) // 写入环形缓冲区尾部
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
selunlock(scases, lockorder)
goto retc
标签 recv / send:与对端 sudog 交接
与 chan.go 中的 recv、send 相同,解锁回调里调用 selunlock。
recv:
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) // 与 chan.go 的 recv 相同
recvOK = true
goto retc
send:
// ... race/msan/asan,见 518–526 行
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) // 与 chan.go 的 send 相同
goto retc
标签 rclose:已关闭且无数据
对应 v, ok := <-ch 中 ok 为 false;若有接收目标地址则清零。
rclose:
selunlock(scases, lockorder)
recvOK = false // 已关闭且无数据,对应 v, ok := <-ch 中 ok 为 false
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
if raceenabled {
raceacquire(c.raceaddr())
}
goto retc
返回与往已关闭 channel 发送
retc 里可做阻塞剖析;sclose 与 chansend 关闭语义一致。
retc:
if caseReleaseTime > 0 {
blockevent(caseReleaseTime-t0, 1) // 阻塞剖析
}
return casi, recvOK
sclose:
selunlock(scases, lockorder)
panic(plainError("send on closed channel")) // 与 chansend 关闭语义一致
}