目录

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 中语句的不同选择了不同的优化路径:

  1. 空的 select 语句会被直接转换成 block 函数的调用,直接挂起当前 Goroutine;
  2. 如果 select 语句中只包含一个 case,就会被转换成 if ch == nil { block }; n; 表达式;
  • 首先判断操作的 Channel 是不是空的;
  • 然后执行 case 结构中的内容;
  1. 如果 select 语句中只包含两个 case 并且其中一个是 default,那么 Channel 和接收和发送操作都会使用 selectnbrecv 和 selectnbsend 非阻塞地执行接收和发送操作;
  2. 在默认情况下会通过 selectgo 函数选择需要执行的 case 并通过多个 if 语句执行 case 中的表达式;

在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 selectgo 函数,这个函数会按照以下的过程执行:

  1. 随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成一个用于遍历的锁定顺序 lockOrder;
  2. 根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel 消息;
  • 如果有消息就直接获取 case 对应的索引并返回;
  • 如果没有消息就会创建 sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的 sendq 和 recvq 队列中并调用 gopark 触发调度器的调度; 当调度器唤醒当前 Goroutine 时就会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 sudog 结构并返回对应的索引;

然而并不是所有的 select 控制结构都会走到 selectgo 上,很多情况都会被直接优化掉,没有机会调用 selectgo 函数。