Go 的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前的groutine。所以,有人也会说select是用来阻塞监听goroutine的。
还有人说:select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。

以上说法都正确。

I/O多路复用

我们来回顾一下是什么是I/O多路复用

普通多线程(或进程)I/O

每来一个进程,都会建立连接,然后阻塞,直到接收到数据返回响应。
普通这种方式的缺点其实很明显:系统需要创建和维护额外的线程或进程。因为大多数时候,大部分阻塞的线程或进程是处于等待状态,只有少部分会接收并处理响应,而其余的都在等待。系统为此还需要多做很多额外的线程或者进程的管理工作。

为了解决图中这些多余的线程或者进程,于是有了"I/O多路复用"

I/O多路复用

每个线程或者进程都先到图中”装置“中注册,然后阻塞,然后只有一个线程在”运输“,当注册的线程或者进程准备好数据后,”装置“会根据注册的信息得到相应的数据。从始至终kernel只会使用图中这个黄黄的线程,无需再对额外的线程或者进程进行管理,提升了效率。

select组成结构

select的实现经历了多个版本的修改,当前版本为:1.11
select这个语句底层实现实际上主要由两部分组成:case语句执行函数
源码地址为:/go/src/runtime/select.go

每个case语句,单独抽象出以下结构体:

type scase struct {
c *hchan // chan
elem unsafe.Pointer // 读或者写的缓冲区地址
kind uint16 //case语句的类型,是default、传值写数据(channel <-) 还是 取值读数据(<- channel)
pc uintptr // race pc (for race detector / msan)
releasetime int64
}

结构体可以用下图表示:


其中比较关键的是:hchan,它是channel的指针。
在一个select中,所有的case语句会构成一个scase结构体的数组。

然后执行select语句实际上就是调用func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)函数。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)函数参数:

  • cas0 为上文提到的case语句抽象出的结构体scase数组的第一个元素地址
  • order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列lockorder。
  • nncases表示scase数组的长度

selectgo返回所选scase的索引(该索引与其各自的select {recv,send,default}调用的序号位置相匹配)。此外,如果选择的scase是接收操作(recv),则返回是否接收到值。

谁负责调用func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)函数呢?

/reflect/value.go中有个func rselect([]runtimeSelect) (chosen int, recvOK bool)函数,此函数的实现在/runtime/select.go文件中的func reflect_rselect(cases []runtimeSelect) (int, bool)函数中:

func reflect_rselect(cases []runtimeSelect) (int, bool) {
//如果cases语句为空,则阻塞当前groutine
if len(cases) == 0 {
block()
}
//实例化case的结构体
sel := make([]scase, len(cases))
order := make([]uint16, 2*len(cases))
for i := range cases {
rc := &cases[i]
switch rc.dir {
case selectDefault:
sel[i] = scase{kind: caseDefault}
case selectSend:
sel[i] = scase{kind: caseSend, c: rc.ch, elem: rc.val}
case selectRecv:
sel[i] = scase{kind: caseRecv, c: rc.ch, elem: rc.val}
}
if raceenabled || msanenabled {
selectsetpc(&sel[i])
}
}
return selectgo(&sel[0], &order[0], len(cases))
}

那谁调用的func rselect([]runtimeSelect) (chosen int, recvOK bool)呢?
/refect/value.go中,有一个func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)的函数,其调用了rselect函数,并将最终Go中select语句的返回值的返回。

以上这三个函数的调用栈按顺序如下:

  • func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)
  • func rselect([]runtimeSelect) (chosen int, recvOK bool)
  • func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)

这仨函数中无论是返回值还是参数都大同小异,可以简单粗暴的认为:函数参数传入的是case语句,返回值返回被选中的case语句。
那谁调用了func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)呢?
可以简单的认为是系统了。
来个简单的图:

前两个函数Selectrselect都是做了简单的初始化参数,调用下一个函数的操作。select真正的核心功能,是在最后一个函数func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)中实现的。

selectgo函数做了什么

打乱传入的case结构体顺序

锁住其中的所有的channel

遍历所有的channel,查看其是否可读或者可写

如果其中的channel可读或者可写,则解锁所有channel,并返回对应的channel数据

假如没有channel可读或者可写,但是有default语句,则同上:返回default语句对应的scase并解锁所有的channel。

假如既没有channel可读或者可写,也没有default语句,则将当前运行的groutine阻塞,并加入到当前所有channel的等待队列中去。

然后解锁所有channel,等待被唤醒。

此时如果有个channel可读或者可写ready了,则唤醒,并再次加锁所有channel,

遍历所有channel找到那个对应的channel和G,唤醒G,并将没有成功的G从所有channel的等待队列中移除。

如果对应的scase值不为空,则返回需要的值,并解锁所有channel

如果对应的scase为空,则循环此过程。

select和channel之间的关系

在想想select和channel做了什么事儿,我觉得和多路复用是一回事儿

更多精彩内容,请关注我的微信公众号 互联网技术窝 或者加微信共同探讨交流:

参考文献:

 
 
 

最新文章

  1. JMM和happens-before原则
  2. 利用 Html 元标记控制搜索引擎蜘蛛
  3. Disaster recovery best practices for Symantec Endpoint Protection 12.1
  4. css样式重写
  5. python接口的调用方法
  6. HDU 5379 Mahjong tree
  7. NOPI读取EXCEL
  8. php中getimagesize函数的用法
  9. WINDOWS硬件通知应用程序的常方法
  10. Angular2 - Starter - Routes, Route Resolver
  11. 如何查看maven plugin所包含的goal
  12. iOS开发之线程间的MachPort通信与子线程中的Notification转发
  13. JAVA_SE基础——29.构造函数
  14. Jmeter(二十)_Mock接口
  15. vue ssr github 项目及其 文章
  16. 【JavaScript】对JS的封装
  17. C/C++常见错误
  18. OO前三次作业分析
  19. 关于pycharm中使用charts无法显示图表的问题(属于个人粗心问题)
  20. webview调起浏览器

热门文章

  1. C++实现根据路径读取文件内容
  2. springboot问题总结
  3. sys模块学习记录
  4. Swoft 容器使用
  5. Django之Form验证
  6. MTK之DrvGen的使用
  7. c语言小项目---通讯录2.0
  8. xml是什么
  9. JS写一个简单日历
  10. mac安装linux双系统的吐槽