1:简介
2:初识
没有case的select或者case为nil的select
package main
func main() {
select {}
}
package main
import "fmt"
func main() {
var ch chan int
select {
case ch <- 1:
fmt.Println("222")
}
}
上面的代码均会有如下报错:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
E:/Server/go/src/any.com/test/t_50.go:8 +0x27
select仅能操作管道
下面是示例代码:
package main
import "fmt"
func main() {
tmp := 3
select {
case tmp == 3:
fmt.Println("222")
}
}
执行之后报错如下:
.\t_50.go:8:11: tmp == 3 evaluated but not used
.\t_50.go:8:11: select case must be receive, send or assign recv
select每次执行只能有一个case执行,而且每个case可能执行的条件都是随机的,下面我们看个例子,当然这个例子是无法证明这个,后面我们会对源码进行分析讲解。
package main
import (
"fmt"
"time"
)
func main() {
//每秒执行一个ticker
t1 := time.NewTicker(1 * time.Second)
//每5秒执行一个ticker
t2 := time.NewTicker(5 * time.Second)
//设置一个通道
ch := make(chan int, 1)
for {
select {
case <-t1.C:
fmt.Println("t1")
for i := 0; i <= 3; i++ {
time.Sleep(6 * time.Second)
fmt.Println(i)
}
case <-t2.C:
fmt.Println("t2")
case ch <- 1:
fmt.Println("ch写入成功")
}
}
}
看下执行结果,都是按照每个case都可能执行到,而且顺序是不确定的。
ch写入成功
t1
0
1
2
3
t2
t1
0
1
2
3
t2
t1
0
1
2
3
t2
......
3:深入源码
我这里的go的版本是1.15.6
每个case语句的数据结构如下:
// Changes here must also be made in src/cmd/internal/gc/select.go's scasetype.
type scase struct {
c *hchan // chan的结构体类型指针
elem unsafe.Pointer //读或者写的缓冲区地址 element
kind uint16 //case语句的类型,是default、传值写数据(channel <-) 还是 取值读数据(<- channel)
pc uintptr // 竞争检查相关用的,go run -race指令
releasetime int64
}
在一个select中,所有的case语句一起组成scase的结构数组,然后执行select语句就是调用func selectgo(cas0 *scase, order0 *uint16, ncases int)函数。
func selectgo(cas0*scase,order0*uint16,ncasesint)(int,bool)函数参数:
selectgo返回所选scase的索引(该索引与其各自的select {recv,send,default}调用的序号位置相匹配),selectgo做了打乱case的结构体顺序,然后会随机选一个执行。如果选择的scase是接收操作(recv),则返回是否接收到值。
selectgo的调用是通过reflect_rselect函数来调用的,reflect_rselect函数的方法是rselect的实现,rselect声明是在/reflect/value.go
rselect的调用是通过/reflect/value.go里面的Select方法来调用的。
关于Select方法的调用当然就是系统来调用了。
关于select语句的底层函数调用流程我们做一下总结:
下面我们来深入看下selectgo的函数,由于selectgo函数比较长,我这里大概列一下重要的点:
对所有的case进行排序,生成随机顺序,代码如下:
// generate permuted order
for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i + 1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}
遍历所有的case语句,如果所有的case都未就绪,则走default,如果没有default,则会阻塞。如果有就绪channel,则直接跳出顺序进行管道操作并返回。核心代码如下:
loop:
// pass 1 - look for something already waiting
var dfli int
var dfl *scase
var casi int
var cas *scase
var recvOK bool
for i := 0; i < ncases; i++ {
casi = int(pollorder[i])
cas = &scases[casi]
c = cas.c
switch cas.kind {
case caseNil:
continue
case caseRecv:
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
if c.qcount > 0 {
goto bufrecv
}
if c.closed != 0 {
goto rclose
}
case caseSend:
if raceenabled {
racereadpc(c.raceaddr(), cas.pc, chansendpc)
}
if c.closed != 0 {
goto sclose
}
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
if c.qcount < c.dataqsiz {
goto bufsend
}
case caseDefault:
dfli = casi
dfl = cas
}
}
if dfl != nil {
selunlock(scases, lockorder)
casi = dfli
cas = dfl
goto retc
}
retc:
if cas.releasetime > 0 {
blockevent(cas.releasetime-t0, 1)
}
return casi, recvOK
4:总结多路复用
我们看下select中的channel实现多路复用的图:
每个线程或者进程都先到图中”装置“中注册,然后阻塞,然后只有一个线程在”运输“,当注册的线程或者进程准备好数据后,”装置“会根据注册的信息得到相应的数据。从始至终kernel只会使用图中这个黄黄的线程,无需再对额外的线程或者进程进行管理,提升了效率。我这里就没有提协程了,因为最终还是系统线程去执行。