select case语句举例_图解Go select语句原理

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

以上说法都正确。

I/O多路复用

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

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

7eedf53f865a5f70ab22c578f20afaf8.png

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

54db311712b2edd5258043909e6ca391.png

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

I/O多路复用

d226658d82fa715cfa9260cced1300a2.png

每个线程或者进程都先到图中”装置“中注册,然后阻塞,然后只有一个线程在”运输“,当注册的线程或者进程准备好数据后,”装置“会根据注册的信息得到相应的数据。从始至终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 

结构体可以用下图表示:

0458755276d4ac2cc4fdc28255e43c50.png

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

a4a4d7249901d608391965acd1299111.png

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

f88cbc342725bc0ce6a949947f22982b.png

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)呢? 可以简单的认为是系统了。 来个简单的图:

37782f2b19665b61011b853ad23673ce.png

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

selectgo函数做了什么

打乱传入的case结构体顺序

671c2bbd098c4aa92b09160d0ec2e03b.png

锁住其中的所有的channel

50cec0683203adc0d521c587807ad265.png

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

c87a892ce670f398da3b357f00b98788.png

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

8b9203c1a6a2e2ebdb21a1a6fde774a9.png
7742f800609ee2158f24b97ce2a562f5.png

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

1857e96ba4feb493b00075a7d411fa94.png

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

cf315ddf19d2fc5b2acb6515c3c1a5ff.png

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

964fc1828f4d42bdea0118c35e6a5ae4.png

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

e47bba1387cf9c61ec16fc40a1f3349f.png

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

8fd12c928b8023ced965a6957191ccfa.png

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

4c202d2200f8b126af6ecd1083edd6e4.png

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

select和channel之间的关系

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

afd84497f0e7393af67d19f9918ab325.png

更多精彩内容,请关注我的微信公众号 互联网技术窝

参考文献:

  • https://my.oschina.net/renhc/blog/2253937
  • https://blog.csdn.net/xd_rbt_/article/details/80287959
  • https://blog.csdn.net/qq_34199383/article/details/80303629
  • https://blog.csdn.net/wangxindong11/article/details/78591308
  • https://draveness.me/golang-select
  • https://studygolang.com/articles/1807

预览

1542 字

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值