(转)深入golang -- select
原文:https://zhuanlan.zhihu.com/p/509148906
老规矩相信大家已经知道 select 应用的特性,这里主要是介绍 select 的底层原理。
select 底层原理主要分为两部:
- select 语句优化
- selectgo
select 语句优化
编译阶段,编译器会根据 select
中 case
的不同,会对控制语句进行优化。这一过程发生在:
// src/cmd/compile/internal/walk/select.go
func walkSelectCases(cases []*ir.CommClause) []ir.Node {
...
}
需要强调一下的是 default 也算是一个 case。
空 select
即 select{}
,语法下的优化:
if ncas == 0 {
return []ir.Node{mkcallstmt("block")}
}
mkcallstmt("block")
就直接转换成了成了 runtime.block()
函数:
// src/runtime/select.go
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) // forever
}
gopark 函数咱们应该很清楚作用了。
(不清楚的,可以回看 GMP调度 那篇文章)
单一case
还有只有一个 case的情况:
select {
case v := <-ch:
}
通过 walkSelectCases()
里面这段逻辑
if ncas == 1 {
...
}
编译器就会将select去掉,直接改写成接收通道的方式:
v := <- ch
非阻塞
还只有一共两个选择, 一个是 case,一个是 default的情况:
if ncas == 2 && dflt != nil {
...
}
编译器优化结果如下:
// select {
// case c <- v:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if selectnbsend(c, v) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}
// select {
// case v = <-c:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if selectnbrecv(&v, c) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
// select {
// case v, ok = <-c:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if c != nil && selectnbrecv2(&v, &ok, c) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
// TODO(khr): just return 2 values from this function, now that it is in Go.
selected, *received = chanrecv(c, elem, false)
return
}
我们看到 selectnbsend()
或者 selectnbrecv()
/ selectnbrecv2()
,都调用的是 我们在 channel 那一章节的 介绍的 发送/接收函数。但不同的是,走的的是非阻塞 channel 逻辑,即 block
参数为 false。
selectgo
其他情况的写法,就是真正走到 select
的源码逻辑中。从 func walkSelectCases()
中可以看到,selectgo
也放到了语法节点中:
func walkSelectCases(cases []*ir.CommClause) []ir.Node {
...
fn := typecheck.LookupRuntime("selectgo")
var fnInit ir.Nodes
r.Rhs = []ir.Node{mkcall1(fn, fn.Type().Results(), &fnInit, bytePtrToIndex(selv, 0), bytePtrToIndex(order, 0), pc0, ir.NewInt(int64(nsends)), ir.NewInt(int64(nrecvs)), ir.NewBool(dflt == nil))}
...
}
接下来咱们就来拆分 selecgo 函数中的逻辑,先看一开始:
// src/runtime/select.go
// selectgo implements the select statement.
//
// cas0 points to an array of type [ncases]scase, and order0 points to
// an array of type [2*ncases]uint16 where ncases must be <= 65536.
// Both reside on the goroutine's stack (regardless of any escaping in
// selectgo).
//
// For race detector builds, pc0 points to an array of type
// [ncases]uintptr (also on the stack); for other builds, it's set to
// nil.
//
// selectgo returns the index of the chosen scase, which matches the
// ordinal position of its respective select{recv,send,default} call.
// Also, if the chosen scase was a receive operation, it reports whether
// a value was received.
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
// NOTE: In order to maintain a lean stack size, the number of scases
// is capped at 65536.
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]
lockorder := order1[ncases:][:ncases:ncases]
...
}
从注释可以看出各个参数的意思,以及 selectgo 返回的是case的索引,即该执行哪个case 下的逻辑。重点关注一下 case0
和 order0
。
case0
的结构体就是 scase
:
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}
本文使用的是go version: 1.17.9
, 该版本下,scase结构体 只有两个字段即一个通道,以及发送/接收的值。
order0
就是我们的 case 数组,会被转化成scases
和 order1
,最终会被转换成 pollorder 和 lockorder 。
pollorder 存放的是: case中元素的索引。
lockorder 存放的是:根据chan在堆区的地址顺序排序(大根堆排序)的所有chan。
(由于每个 case 中的 c 都会上锁,又按地址顺序排序,用顺序锁有效避免协程并发带来的死锁问题。)
还有一段注释很重要,就是 case数量不能超过 65536。
然后就是 selectgo()
的执行逻辑。虽然代码很多,但总结起来主要就干4件事:
- 将case乱序后,加入lockerorder中。
- 第一次循环执行 pollorder 中已经乱序了的 case -- 对就绪的channel进行 发送/接收 。
- 第二次循环执行 lockorder,将当前 goroutine 加入到 所有 case 的 channel 发送/接收队列中( sendq/recvq), 等待被唤醒。
- goroutine 被唤醒之后,找到满足条件的 channel并处理。
case 乱序
我们再来具体看三处逻辑的源码。先看事情1:
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]
...
norder := 0
// 第一个for
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)) // fastrandn 随机函数
pollorder[norder] = pollorder[j]
pollorder[j] = uint16(i)
norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]
...
// 第二个for
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
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