54

图解Go select语句原理

 5 years ago
source link: https://studygolang.com/articles/19416?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

以上说法都正确。

I/O多路复用

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

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

NrY3Ajz.png!web

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

vYnIFru.png!web

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

I/O多路复用

z2aAb2U.png!web

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

结构体可以用下图表示:

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

veyuIbN.png!web

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

MrYfaib.png!web

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], ℴ[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) 呢? 可以简单的认为是系统了。 来个简单的图: nmaE32r.png!web

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

selectgo函数做了什么

打乱传入的case结构体顺序

qq26F3e.png!web

锁住其中的所有的channel nYv263a.png!web

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

neu6zmR.png!web

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

fEj6FbM.png!web

mmqiAbR.png!web

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

iyMNbmq.png!web

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

Z3iqqmy.png!web

然后解锁所有channel,等待被唤醒。 UJvuUrV.png!web

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

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

nq2AJfU.png!web

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

RBRvErr.png!web

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

select和channel之间的关系

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

FRBreey.png!web

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

z6V3iiJ.jpg!web

参考文献:

  • 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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK