golang select底层原理
前言
select
是操作系统中的系统调用,我们经常会使用 select
、poll
和 epoll
等函数构建 I/O 多路复用模型提升程序的性能。Go 语言的 select
与操作系统中的 select
比较相似,但也有不同点,它只支持channel收发的多路复用。
这里已go1.19版本为例,编译器在中间代码生成期间会根据 select
中 case
的不同对控制语句进行优化,这一过程都发生在 cmd/compile/internal/select/walk.go
的walkSelectCases()函数中,我们在这里会分四种情况介绍处理的过程和结果。
不存在任何case
select {
}
// cmd/compile/internal/select/walk.go
func walkselectcases(cases *Nodes) []*Node {
n := cases.Len()
if n == 0 {
return []*Node{mkcall("block", nil, nil)}
}
...
}
它直接将类似 select {}
的语句转换成调用 runtime.block
函数:
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}
只有一个case
如果当前的 select
条件只包含一个 case
,那么编译器会将 select
改写成 if
条件语句。下面对比了改写前后的代码:
// 改写前
select {
case v, ok <-ch: // case ch <- v
...
}
// 改写后
if ch == nil {
block()
}
v, ok := <-ch // case ch <- v
...
一个为正常case,一个为defalt case
编译后改写为:
select {
case ch <- i:
...
default:
...
}
if selectnbsend(ch, i) {
...
} else {
...
}
selectnbsend和selectnbrecv实际上是一个非阻塞式地读写channel,在channel底层原理有介绍
至少有两个case
分为两种情况:
- 两个case都不是default
- 至少有三个case,且里面包含一个default
package main
import "fmt"
import "time"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(5 * time.Second)
ch2 <- 1
}()
select {
case <-ch1:
fmt.Println("chan1 recv")
case <-ch2:
fmt.Println("chan2 recv")
}
fmt.Println("done")
}
此时两个channel都处于阻塞状态,这时候就是体现go的select实现多路复用的时候了。
主线程在执行select时,调用栈顺序如下:
// src/reflect/value.go
func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) {
// ...
chosen, recvOK = rselect(runcases)
// ...
return chosen, recv, recvOK
}
func rselect([]runtimeSelect) (chosen int, recvOK bool)
// src/runtime/select.go
//go:linkname reflect_rselect reflect.rselect
func reflect_rselect(cases []runtimeSelect) (int, bool) {
if len(cases) == 0 {
block()
}
// ...
chosen, recvOK := selectgo(&sel[0], &order[0], pc0, nsends, nrecvs, dflt == -1)
// ...
return chosen, recvOK
}
这里来分析下 selectgo 的具体实现;
1. 打乱case顺序
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
// 生成随机顺序
norder := 0
for i := range scases {
cas := &scases[i]
// 忽略轮询和锁定命令中没有通道的情况
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]
// 根据 channel 地址进行排序,决定获取锁的顺序
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]
}
...
// 锁定所有的channel
sellock(scases, lockorder)
...
}
打乱顺序后,可以实现随机的执行。
2. 找出已经 ready 的 case
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
var (
gp *g
sg *sudog
c *hchan
k *scase
sglist *sudog
sgnext *sudog
qp unsafe.Pointer
nextp **sudog
)
// pass 1 - 遍历所有 scase,确定已经准备好的 scase
var casi int
var cas *scase
var caseSuccess bool
var caseReleaseTime int64 = -1
var recvOK bool
// 因为上面已经将scases随机写入到pollorder中
// 所以这里的遍历相比于原 cas0的顺序,就是随机的
for _, casei := range pollorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
// 接收数据
if casi >= nsends {
// 有 goroutine 等待发送数据
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
// 缓冲区有数据
if c.qcount > 0 {
goto bufrecv
}
// 通道关闭
if c.closed != 0 {
goto rclose
}
// 发送数据
} else {
if raceenabled {
racereadpc(c.raceaddr(), casePC(casi), chansendpc)
}
// 判断通道的关闭情况
if c.closed != 0 {
goto sclose
}
// 接收等待队列有 goroutine
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
// 缓冲区有空位置
if c.qcount < c.dataqsiz {
goto bufsend
}
}
}
// 如果不阻塞,意味着有 default,准备退出select
if !block {
selunlock(scases, lockorder)
casi = -1
goto retc
}
...
bufrecv:
// 可以从 buffer 接收
if raceenabled {
if cas.elem != nil {
raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
}
racenotify(c, c.recvx, nil)
}
if msanenabled && cas.elem != nil {
msanwrite(cas.elem, c.elemtype.size)
}
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:
// 可以发送到 buffer
if raceenabled {
racenotify(c, c.sendx, nil)
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.size)
}
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:
// 可以从一个休眠的发送方 (sg)直接接收
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncrecv: cas0=", cas0, " c=", c, "\n")
}
recvOK = true
goto retc
rclose:
// 在已经关闭的 channel 末尾进行读
selunlock(scases, lockorder)
recvOK = false
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
if raceenabled {
raceacquire(c.raceaddr())
}
goto retc
send:
// 可以向一个休眠的接收方 (sg) 发送
if raceenabled {
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.size)
}
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncsend: cas0=", cas0, " c=", c, "\n")
}
goto retc
retc:
if caseReleaseTime > 0 {
blockevent(caseReleaseTime-t0, 1)
}
return casi, recvOK
sclose:
// 向已关闭的 channel 进行发送
selunlock(scases, lockorder)
panic(plainError("send on closed channel"))
}
遍历每一个scase,如果有一个已经ready了,就返回,结束select,以读取为例:
- 1、如果有发送的 goroutine 在等待数据的接收,那么直接从这个 goroutine 中读出数据,结束 select;
- 2、如果 channel 的缓冲区有数据,在缓冲去读出数据, 结束 select;
- 3、如果 channel 关闭了,读出零值,结束 select。
可以看出select中的接收流程和channel的接收流程有点区别,channel的接收流程里是直接调用src/runtime/chan.go里的chanrecv()方法,该方法里有阻塞的情况,而select的接收流程里先是处理了所有非阻塞的情况,只要有一个scase是非阻塞的则返回,否则最后会gopark住当前协程
3. case 都没 ready,且没有 default
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
// pass 2 - 所有 channel 入队,等待处理
gp = getg()
if gp.waiting != nil {
throw("gp.waiting != nil")
}
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
// 获取一个 scase
cas = &scases[casi]
// 监听的 channel
c = cas.c
// 构建sudog,设置这一次阻塞发送的相关信息
sg := acquireSudog()
sg.g = gp
sg.isSelect = true // 标注这个sodug是由select产生的
sg.elem = cas.elem
sg.releasetime = 0
if t0 != 0 {
sg.releasetime = -1
}
sg.c = c
// 按锁定顺序构造等待列表。
*nextp = sg
nextp = &sg.waitlink
if casi < nsends {
c.sendq.enqueue(sg)
} else {
c.recvq.enqueue(sg)
}
}
// goroutine 陷入睡眠,等待某一个 channel 唤醒 goroutine
gp.param = nil
atomic.Store8(&gp.parkingOnChan, 1)
// 阻塞当前协程(gp),直到被其他goroutine唤醒
gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
...
}
- 对每个scase构造一个对应的sudog,并把这些sudog分别分放到对应的channel等待队列中;
- gopark阻塞该协程,直到有任意一个操作channel的协程来唤醒这个协程。
4. 唤醒后返回 channel 对应的 case
func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) {
// ...
gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1)
// 对所有的 channel 加锁
sellock(scases, lockorder)
// 之前已经有很多sudog被放入chan的等待队列里,现在只有一个会被唤醒,那哪个是被唤醒的呢?
// 关键逻辑:可以通过gp.param来获取已解除阻塞的那个sudog
sg = (*sudog)(pg.param)
gp.param = nil
// waiting 链表按照 lockorder 顺序存放着 sudog
sglist = gp.waiting
casi = -1
cas = nil // cas 便是唤醒 goroutine 的 case
for _, casei := range lockorder {
k = &scases[casei]
if k.kind == caseNil {
continue
}
// 如果相等说明,goroutine 是被当前 case 的 channel 收发操作唤醒的
// 如果是关闭操作,那么 sg 为 nil, 不会对 cas 赋值
if sg == sglist {
casi = int(casei)
cas = k
} else {
// goroutine 已经被唤醒,将 sudog 从相应的收发队列中移除
c = k.c
// func (q *waitq) dequeueSudoG(sgp *sudog)
// dequeueSudoG 会通过 sudog.prev 和 sudog.next 将 sudog 从等待队列中移除
if k.kind == caseSend {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
// 释放 sudog,然后准备处理下一个 sudog
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}
...
}
唤醒后的操作流程:
- 对所有的channel加锁;
- 获取当前的sudog,sudog里包含channel信息;
- 遍历每个scase,如果goroutine 是被当前 case 的 channel 收发操作唤醒的,则不做处理;
- 否则从这个channel中移除这个sudog;
- 对所有的channel解锁。
因为已经找到了一个可执行的 case,剩下的 case 中没有被用到的 sudog 就会被忽略并且释放掉。为了不影响 Channel 的正常使用,我们还是需要将这些废弃的 sudog 从 Channel 中出队。