go select
目录
select使用
- select只会执行一次
- case语句必须是对channel的操作
- case语句不管是接收还是发送,语句表达式都会执行(执行顺序是从左到右,从上到下,这里只是语句表达式,而不是发送和接收操作的顺序,判断发送和接收是随机的顺序)
- 会对所有case语句进行判断,如果多个都符合,随机选一个
- 如果case都不符合,default(如果存在,不存在就会阻塞等着)执行
func test1(ctx context.Context) {
for {
select {
case <-ctx.Done(): //context到期或主动取消时,channel就被关闭,零值被取出,语句得到执行
fmt.Println(ctx.Err() == context.DeadlineExceeded)
fmt.Println("test1 stop")
return
case <-time.Tick(time.Second):
fmt.Println("tick")
default:
fmt.Println("test1")
time.Sleep(time.Second)
}
}
}
case表达式都会执行,验证:
func main() {
select {
case getChan("this is 1 get chan") <- getInt("this is 1 get int"):
//getChan(),getInt都会执行,从左到右,从上到下
fmt.Println("1 被选中")
case getChan("this is 2 get chan") <- getInt("this is 2 get int"):
fmt.Println("2 被选中")
default:
fmt.Println("default 被选中")
}
}
func getChan(s string) chan int {
fmt.Println(s)
c1 := make(chan int, 1)
return c1
}
func getInt(s string) int {
fmt.Println(s)
return 1
}
select原理
定义了一个数据结构表示每个case语句(含defaut,default实际上是一种特殊的case),select执行过程可以类比成一个函数,函数输入case数组,输出选中的case,然后程序流程转到选中的case块。 case数据结构
type scase struct {
c *hchan // chan
kind uint16
elem unsafe.Pointer // data element
}
-
scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。 scase.kind表示该case的类型,分为读channel、写channel和default,三种类型分别由常量定义:
-
caseRecv:case语句中尝试读取scase.c中的数据; caseSend:case语句中尝试向scase.c中写入数据; caseDefault: default语句 scase.elem表示缓冲区地址,跟据scase.kind不同,有不同的用途:
-
scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址; scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;
真正选择case的函数是selectgo函数,
总结
select 结构的执行过程与实现原理,首先在编译期间,Go 语言会对 select 语句进行优化,以下是根据 select 中语句的不同选择了不同的优化路径:
- 空的 select 语句会被直接转换成 block 函数的调用,直接挂起当前 Goroutine;
- 如果 select 语句中只包含一个 case,就会被转换成 if ch == nil { block }; n; 表达式;
- 首先判断操作的 Channel 是不是空的;
- 然后执行 case 结构中的内容;
- 如果 select 语句中只包含两个 case 并且其中一个是 default,那么 Channel 和接收和发送操作都会使用 selectnbrecv 和 selectnbsend 非阻塞地执行接收和发送操作;
- 在默认情况下会通过 selectgo 函数选择需要执行的 case 并通过多个 if 语句执行 case 中的表达式;
在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 selectgo 函数,这个函数会按照以下的过程执行:
随机
生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成一个用于遍历的锁定顺序 lockOrder;- 根据 pollOrder
遍历所有的
case 查看是否有可以立刻处理的 Channel 消息;
- 如果有消息就直接获取 case 对应的索引并返回;
- 如果没有消息就会创建 sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的 sendq 和 recvq 队列中并调用 gopark 触发调度器的调度; 当调度器唤醒当前 Goroutine 时就会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 sudog 结构并返回对应的索引;
然而并不是所有的 select 控制结构都会走到 selectgo 上,很多情况都会被直接优化掉,没有机会调用 selectgo 函数。