Go 的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前的groutine。因此,有人也会说select是用来阻塞监听goroutine的。 还有人说:select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。golang
以上说法都正确。数组
咱们来回顾一下是什么是I/O多路复用
。微信
每来一个进程,都会创建链接,而后阻塞,直到接收到数据返回响应。 普通这种方式的缺点其实很明显:系统须要建立和维护额外的线程或进程。由于大多数时候,大部分阻塞的线程或进程是处于等待状态,只有少部分会接收并处理响应,而其他的都在等待。系统为此还须要多作不少额外的线程或者进程的管理工做。多线程
为了解决图中这些多余的线程或者进程,因而有了"I/O多路复用"函数
每一个线程或者进程都先到图中”装置“中注册,而后阻塞,而后只有一个线程在”运输“,当注册的线程或者进程准备好数据后,”装置“会根据注册的信息获得相应的数据。从始至终kernel只会使用图中这个黄黄的线程,无需再对额外的线程或者进程进行管理,提高了效率。ui
select的实现经历了多个版本的修改,当前版本为:1.11 select这个语句底层实现实际上主要由两部分组成:case语句
和执行函数
。 源码地址为:/go/src/runtime/select.gospa
每一个case语句,单独抽象出如下结构体:.net
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)
函数。3d
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
函数参数:
scase
数组的第一个元素地址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)
呢? 能够简单的认为是系统了。 来个简单的图:
前两个函数Select
和rselect
都是作了简单的初始化参数,调用下一个函数的操做。select真正的核心功能,是在最后一个函数func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
中实现的。
打乱传入的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作了什么事儿,我以为和多路复用是一回事儿
互联网技术窝
或者加微信共同探讨交流:参考文献: