为什么select仅能作用于管道?
select的特性⌗
case的执行⌗
具体执行那个case,取决于函数传入的管道
- 管道没有缓冲区
- 有缓冲区并且还可以塞数据
- 有缓冲区但缓冲区已满
- 有缓冲区且已有部分数据
返回值⌗
可以在case中声明变量并赋值
default⌗
default不能处理管道读写 当所有case被阻塞,执行default default是特殊的case
使用案例⌗
永久阻塞⌗
使用select阻塞main函数
func main() {
server := webhooktesting.NewTestServer(nil)
server.StartTLS()
fmt.Println("serving on", server.URL)
select {} // 没有case和default ,main()永久阻塞
}
快速检错⌗
当使用管道来传输error时, 可以使用select快速检查管道中是否有error
errCh := make(chan error, active)
jm.deleteJobPods(&job, activePods, errCh) // 传入chan用于记录error
select {
case manageJobErr = <-errCh:
if manageJobErr != nil {
break
}
default: // 没有error, 结束
}
限时等待⌗
当使用管道管理函数上下文时, 可以使用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
}
实现原理⌗
- 为什么每个case语句只能处理一个管道?
- 为什么case语句执行顺序随机(多个case都就绪的情况下)?
- 为什么case语句向值为nil的管道中写数据不会触发panic?
数据结构⌗
type scase struct {
c *hchan // 操作的管道
kind unit16 // case类型
elem unsafe.Pointer // 指向数据存放位置的指针
...
}
- 管道 因为每个scase中只有一个管道, 这决定了一个case只能处理一个管道 编译器在处理case语句时,如果case语句中没有管道操作(不能处理成scase对象),则会给出编译错误:
select case must be receive, send or assign recv
- 类型
const (
caseNil = iota
caseRecv
caseSend
caseDefault
)
- caseNil: 表示其操作的管道值为nil, 由于nil管道既不可读,也不可写,所以永远不会命中 所以在case语句中向值为nil的管道中写数据不会触发panic的原因
- caseRecv: 从管道中读数据
- caseSend: 向管道中写数据
- caseDeafult: 不会操作管道,每个select中仅可存在一个, 可出现在任意位置
- 数据 scase.elem表示数据存放的地址
- caseRecv: 读出的数据存放的地址
- caseSend: 将写入管道的数据存放的地址
实现逻辑⌗
selectgo()
用于处理select语句
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
selectgo()
函数会从一组case中挑选一个case,并返回命中case的下标,对于caseRecv的case,还会返回是否读取成功
当所有case都不可能就绪时,selectgo()陷入永久阻塞,此时函数不会返回
cas0⌗
编译器会将select中的case语句存储在一个数组中,cas0保存这个数组的地址
ncases⌗
ncases表示case的个数(包活default),即cas0数组的长度
order0⌗
order0 为一个整型数组的地址,其长度为case格式的2倍. order0数组是case执行随机性的关键
order0数组被一分为二 前半部分存放case的随机顺序(pollorder),selectgo()会将原始的case顺序打乱,这样在检查每个case是否就绪时就会表现出随机性 后半部分存放管道加锁的顺序(lockorder),selectgo()会按照管道地址顺序对多个管道加锁,从而避免因重复加锁引发的死锁问题
实现⌗
selectgo()函数的实现包括以下要点:
- 通过随机函数fastrandn()将原始的case顺序打乱,在遍历各个case时使用打乱后的顺序就会表现出随机性
- 循环遍历各个case时,如果发现某个case就绪,则直接跳出循环操作管道并返回
- 循环遍历各个case时,如果循环正常结束,说明所有case都没有就绪,有default直接跳default
- 如果都没有就绪且没有default,selectgo()将阻塞等待所有管道,任一管道就绪后,都将开始新的循环
小结⌗
- select仅能操作管道
- 每个case仅能处理一个管道,要么读要么写
- 多个case语句的执行顺序是随机的
- 存在default,则select不会阻塞
Read other posts