【Go专家编程——控制结构——select】

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将不会阻塞
  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值