1. select
select是Go在语言层面提供的多路I/O复用机制,用于检测多个管道是否就绪(即可读可写),其特性与管道息息相关。
case1
- 当有多个case是就绪时,随机执行一个case
c1 := make(chan int,10)
c2 := make(chan int,10)
c1 <- 1
c2 <- 2
select{
case <- c1:
fmt.Printf("c1")
case <- c2:
fmt.Printf("c2")
}
case2
- 如果管道没有缓冲区,所以即不可读也不可写
c := make(chan int)
select{
case <- c:
fmt.Printf("readable")
case <- 1:
fmt.Printf("writable")
}
除此之外还有的特性:
- 已关闭的管道仍可以读取
- 从chan中获取数据最多可以返回两个变量,第一个变量代表值,第二个变量的含义为是否成功读取了数据
- 空的select语句会永远阻塞
- 如果case语句操作了nil管道,那么该case语句会被忽略,函数会从default出口返回。如果所有的语句都被忽略了,且没有default语句,则等同于空select语句。
2. 特性速览
2.1 select特性
管道读写,select只能作用于管道,包括数据读取和写入。
func SelectForChan(c chan string){
var recv string
send := "Hello"
select{
case recv = <-c:
fmt.Println(recv)
case c <- send:
fmt.Println(send)
}
}
- 第一种情况:管道没有缓冲区
- 阻塞
- 第二种情况:管道有缓冲区且还可以存放至少一个数据,管道中无数据
- 执行第二个case语句
- 第三种情况:管道有缓冲区且缓冲区中已放满数据
- 执行第一个case语句
- 第四种情况:管道有缓冲区且缓冲区已有部分数据且还可以存入数据
- 随机挑选一个case语句执行
default
- default语句不能处理管道读写操作
- 当所有case语句都阻塞时,default语句将被执行
2.2 使用举例
永久阻塞
有时候我们启动协程处理任务,并且不希望main函数退出,此时就可以让main函数永久性陷入阻塞。
select{}
快速检错
有时候我们会使用管道来传输错误,此时就可以使用select语句快速检查管道中是否有错误,并且避免陷入循环。
errCh := make(chan error,active)
//传入管道用于记录错误
jm.deleteJobPods(&job, activePods, errCh)
select{
case manageJobErr = <- errCh: // 检查是否有错误发生
if manageJobErr != nil{
break
}
}
default: //没有错误,快速结束检测
如果没有错误,也不会陷入阻塞
限时等待
使用管道来管理函数的上下文,可以使用select来创建只有一定时效的管道
func waitForStopOrTimeout(stopCh <- chan struct{},timeout time.Duration) <- chan struct{}{
stopChWithTimeout := make(chan struct{})
go func(){
select {
case <- stopCh://自然结束
case <- time.After(timeout)://最长等待时间结束
}
close(stopChWithTimeout)
}
return stopChWithTimeout
}
该函数返回一个管道,用于在函数之间传递,但该管道会在指定时间后自动关闭。
3. 实现原理
待回答的问题
- 为什么每个case语句只能处理一个管道
- 为什么case语句的执行顺序是随机的(多个case都就绪的情况下)
- 为什么case语句中向值为nil的管道中写数据不会触发panic
3.1 数据结构
select中的case语句对应于runtime包中的scase数据结构
type scase struct{
c *hchan //操作的管道
kind uint16 //case类型
elem unsafe.Pointer //date elemen
}
//scase的成员变量kind表示case语句的类型,每一类型均表示一类管道操作或特殊case
const (
caseNil = iota //管道的值为nil
caseRecv //读管道的case
caseSend //写管道的case
caseDefault //default语句
)
- default语句可以出现在任意位置,在编译的时候会默认放到最后执行
- elem表示数据存放的位置
- 在类型为caseRecv的case中,elem表示从管道读出的数据的存放地址
- 在类型为caseSend的case中,elem表示将写入管道的数据的存放地址
3.2 实现逻辑
Go在运行时包中提供selectgo()方法用于处理select语句
func selectgo(cas0 *scase,order0 *uint16,ncases int) (int, bool)
- selectgo会从一组case语句中挑选一个case,并返回命中case的下标,对于caseRecv的case,还会返回是否成功地从管道中读取数据
- 参数解读
- cas0:编译器会将case语句存储为一个数组,cas0就上数组的地址
- order0:是一个整型数组地址,其长度为case个数的2倍,是case随机执行的关键。前半段存放case遍历顺序,后半段是管道锁定孙旭
- ncases表示case的个数(包括default),即cas0的长度
伪代码如下:
func selectgo(cas0 *scase,order0 *uint16,ncases int) (int, bool){
scases := cas0[:ncases:ncases]//扩展表达式
pollorder := order0[:ncases:ncases]//切取order0前半段,用于保存随机顺序
//过滤掉管道值为nil的case
for i:= range scases {
cas := &scases[i]
if cas.c == nil && cas.kind != caseDefault {
*cas = scase{}
}
}
// 生成case的随机顺序,保存到pollorder中
for i := 1;i<ncases;i++{
j := fastrandn(uint32(i + 1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}
loop:
// 开始循环遍历各个case
var diff int //default 语句下标
var dfl *scase //default 语句对应的scase
var casi int //case 下标
var cas *scase //case 语句
var recvOK bool
for i:=0;i<ncases;i++{
casi = int(pollorder[i])
cas = &scases[casi]
c = cas.c //对应的管道
switch cas.kind{
case caseNil://前面已经过滤掉的case,直接忽略
continue
case caseRecv:
if c 可读{
goto bufrecv//跳出循环,处理读操作
}
case caseSend:
if c 可写{
goto bufsend//跳出循环,处理写操作
}
case caseDefault: //标记default case的位置
dfli = casi
dfl = cas
}
}
//所有的case都未就绪,如存在default出口
if dfl != nil{
casi = dfli
cas = dfl
goto retc
}
bufrecv:
//略去具体的读管道操作
recvOK = true
return casi,recvOK
bufsend:
//略去具体的写管道操作
return casi,recvOK
}
4.小结
- select仅能操作管道
- 每个case语句仅能处理一个管道,要么读,要么写
- 多个case语句的执行顺序是随机的
- 存在default语句,select将不会阻塞