教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang学习之--select--case 原理【转载】

golang学习之--select--case 原理【转载】

发布时间:2022-02-04   编辑:jiaochengji.com
教程集为您提供golang学习之--select--case 原理【转载】等资源,欢迎您收藏本站,我们将为您提供最新的golang学习之--select--case 原理【转载】资源

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

以上说法都正确。

<h2>I/O多路复用</h2>

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

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

image

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

image

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

<h3>I/O多路复用</h3>

image

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

<h2>select组成结构</h2>

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

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

 

<pre><code>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 } </code></pre>

结构体可以用下图表示:

 

image


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

 

image

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

image

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

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

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

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

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

 

<pre><code>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)) } </code></pre>

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

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

<ul><li><code>func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)</code></li><li><code>func rselect([]runtimeSelect) (chosen int, recvOK bool)</code></li><li><code>func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)</code></li></ul>

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

image

 

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

<h3>selectgo函数做了什么</h3>

打乱传入的case结构体顺序

image

锁住其中的所有的channel

 

image

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

image

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

image

image

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

image

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

image

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

 

image

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

 

image

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

image

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

image

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

<h3>select和channel之间的关系</h3>

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

 

到此这篇关于“golang学习之--select--case 原理【转载】”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
29. Go 语言中的 select 用法
golang学习之--select--case 原理【转载】
用一个生产与消费例子学习go语言中goroutine,channel,select,time
Golang并发模型:select进阶
golang面试官:for select时,如果通道已经关闭会怎么样?如果select中只有一个case呢?
Golang中WaitGroup、Context、goroutine定时器及超时学习笔记
Golang 学习笔记:流程控制
从零学习 Go 语言(33):如何手动实现一个协程池?
golang for循环_golang面试官:for select时,如果通道已经关闭会怎么样?如果select中只有一个case呢?...
golang select 的温柔陷阱

[关闭]
~ ~