select是go在语言层面提供地多路IO复用地机制,可以检测多个channel是否ready。

实现原理

case数据结构

源码包src/runtime/select.go:scase定义了表示case地数据结构:

type scase struct {
	c *hchan
	kind uint16
	elem unsafe.Pointer
}

scase.c为当前case语句所操作地channel指针,这也说明一个case语句只能操作一个channel。case.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的数据存放地址

select实现逻辑

select选择case的函数定义在源码src/runtime/select.go:selectgo()中,函数原型如下:

func selectgo(cas0 *scase, order0 *uint16, nacases int) (int, bool)

函数参数:

  • cas0为scase数组的首地址,selectgo()就是从这些scase中找出一个返回。
  • order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列的lockorder:
    • pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。
    • lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的
  • ncase表示scase数组的长度

函数返回值:

  • 返回值int:选中case的编号,这个case编号跟代码一致。
  • bool:是否成功从channel中读取了数据,如果选中的是case是从channel中读数据,则该返回值表示是否读取陈公公。

selectgo的实现用伪代码可以表示为:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
	//1. 锁定scase中所有的channel
	//2. 按照随机顺序检测scase中的channel是否ready
		//2.1 如果case可读,则读取chanle中数据,解锁所有的channel,然后返回(case index, true)
		//2.2 如果case可写,则将数据写入channel,解锁所有channel,然后返回(case index, false)
		//2.3 所有的case都未ready,则解锁所有的channel,然后返回(default index, false)
	//3. 所有case都未ready,且没有default语句
		//3.1 将当前协程加入到所有channel的等待队列
		//3.2 当将协程转入阻塞,等待被唤醒
	//4. 唤醒后返回channel对应的case index
		//4.1 如果是读操作,解锁所有的channel,然后返回(case index, true)
		//4.2 如果是写操作,解锁所有的channel,然后返回(case index, false)
}

对于读channel的case来说,如case elem, ok := <-chan1:,如果channel可能被其他协程关闭,那么就需要检测是否成功,因为close的channel也会返回,不过ok==false

总结

  • select语句中除defaul外,每个case操作一个channel,要么读要么写
  • select语句中除default外,各case执行顺序是随机的。
  • select语句中如果没有defautl语句,则会阻塞等待任一case。
  • select语句中读操作要判断是否成功读取,关闭的channel也可以读取。