select语法总结 select对应的每个case如果有已经准备好的case 则进行chan读写操作;若没有则执行defualt语句;若都没有则阻塞当前goroutine,直到某个chan准备好可读或可写,完成对应的case后退出。
Select的内存布局
了解chanel的实现后对select的语法有个疑问,select如何实现多路复用的,为什么没有在第一个channel操作时阻塞 从而导致后面的case都执行不了。为了解决疑问,对应代码看一下汇编调用了哪些runtime层的函数,发现select语法块被编译器翻译成了以下过程。
创建select–>注册case–>执行select–>释放select
1 | select { |
1 | runtime.newselect |
select实际上是个hselect结构体,其中注册的case放到scase中。scase保存有当前case操作的hchan。pollorder指向的是乱序后的scase序号。lockorder中将要保存的是每个case对应的hchan的地址。
1 | type hselect struct { |
select最后是[1]scase表示select中只保存了一个case的空间,说明select只是个头部,select后面保存了所有的scase,这段Scases的大小就是tcase。在go runtime实现中经常看到这种头部+连续内存的方式。
select的实现
select创建
在newSelect对象时已经知道了case的数目,并已经分配好上述空间。
1 | func selectsize(size uintptr) uintptr { |
注册case
case channel有三种注册 selectsend
selectrecv
selectdefault
,分别对应着不同的case。他们的注册方式一致,都是ncase+1,然后按照当前的index填充scases域的scase数组的相关字段,主要是用case中的chan和case类型填充c和kind字段。
1 | func selectsendImpl(sel *hselect, c *hchan, pc uintptr, elem unsafe.Pointer, so uintptr) { |
select执行
pollorder保存的是scase的序号,乱序是为了之后执行时的随机性。
lockorder保存了所有case中channel的地址,这里按照地址大小堆排了一下lockorder对应的这片连续内存。对chan排序是为了去重,保证之后对所有channel上锁时不会重复上锁。
select语句执行时会对整个chanel加锁
select语句会创建select对象 如果放在for循环中长期执行可能会频繁的分配内存
select执行过程总结如下:
- 通过pollorder的序号,遍历scase找出已经准备好的case。如果有就执行普通的chan读写操作。其中准备好的case是指可以不阻塞完成读写chan的case,或者读已经关闭的chan的case。
- 如果没有准备好的case,则尝试defualt case。
- 如果以上都没有,则把当前的G封装好挂到scase所有chan的阻塞链表中,按照chan的操作类型挂到sendq或recvq中。
- 这个G被某个chan唤醒,遍历scase找到目标case,放弃当前G在其他chan中的等待,返回。
1 | func selectgoImpl(sel *hselect) (uintptr, uint16) { |