[toc]
Go语言:select语句
语法
功能:select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。
语法:
select {
case clauseA:
statementA
case clauseB:
statementB
default:
statementC
}
说明:
- case 中表达式必须是 Channel 收发操作。
- 无论哪一个表达式返回,都会立刻执行 case 中的代码。
- 当 select 中两个 case 同时被触发时,会随机执行其中一个,从而避免饥饿问题发生。
- default 语句:提供非阻塞的收发的特性。若存在可收发的 Channel ,直接处理该 case 的语句;否则,执行 default 中的语句。
select 能够让 Goroutine 同时等待多个 Channel 可读或可写,在多个文件或者 Channel 状态改变之前,select 会一直阻塞当前线程或 Goroutine 。
select 的几种使用场景
阻塞操作
使用空的 select 语句,会直接阻塞当前 Goroutine,导致 Goroutine 进入无法被唤醒的永久休眠状态。
func example() { select {} }
单一管道
select 中只包含一个 case ,编译器会将 select 改写成 if 条件语句。若 case 中 Channel 是空指针时,会直接挂起当前 Goroutine 并陷入永久休眠。
func example() { c := make(chan string) go func() { time.Sleep(time.Second) c <- "one message" } select { case msg := <- c: fmt.Println("received", msg) } // 等价于 // if msg := <- c { // fmt.Println("received", msg) // } }
非阻塞操作:
select中包含多个case,且其中一个是 default 语句。若不存在可收发的 Channel ,直接执行 default 中的语句。
func example() { c := make(chan string) select { case msg := <- c: fmt.Println("received", msg) default: fmt.Println("no message received") } }
编译器对 select 的处理
编译器对 select 语句的一般处理流程:
- 将所有 case 转换成包含 Channel 以及类型等信息的
runtime.scase
结构体。 - 调用
runtime.selectgo
从多个就绪的 Channel 中选择一个可执行的runtime.scase
结构体。 - 通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case 。
随机执行的关键运行时函数:runtime.selectgo
,它的执行步骤为,
- 确定 case 的处理顺序
- 轮询顺序:通过
runtime.fastrandom
函数引入随机性。 - 加锁顺序:按照 Channel 的地址排序后确定加锁顺序。
- 轮询顺序:通过
- 根据轮询顺序遍历所有的 case ,查看是否有可立即处理的 Channel 。
- 如果存在,直接获取 case 对应的索引并返回
- 否则,创建
runtime.sudog
结构体,并将当前 Goroutine 加入到所有相关的 Channel 收发队列,并调用runtime.gopark
挂起当前 Goroutine ,等待调度器的唤醒。
- 当调度器唤醒当前 Goroutine 时,会再次按照加锁顺序遍历所有的 case ,从中查找需要被处理的
runtime.sudog
对应的索引。
select 关键字是 Go 语言特有的控制结构,需要编译器和运行时函数共同实现。