欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

go select原理

程序员文章站 2024-02-11 21:16:40
...

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

以上说法都正确

1、I/O多路复用

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

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

(2)I/O多路复用

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

2、select组成结构

常见代码:

	ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch1 <- 1
    select {
    case e1 := <-ch1:
        //如果ch1通道成功读取数据,则执行该case处理语句
        fmt.Printf("1th case is selected. e1=%v", e1)
    case e2 := <-ch2:
        //如果ch2通道成功读取数据,则执行该case处理语句
        fmt.Printf("2th case is selected. e2=%v", e2)
    default:
        //如果上面case都没有成功,则进入default处理流程
        fmt.Println("default!.")
    }

select这个语句底层实现实际上主要由两部分组成:case语句执行函数
每个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
}

结构体可以用下图表示:
go select原理
其中比较关键的是: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 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)

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

selectgo函数做了什么

打乱传入的case结构体顺序
go select原理
锁住其中的所有的channel
go select原理
遍历所有的channel,查看其是否可读或者可写

go select原理
如果其中的channel可读或者可写,则解锁所有channel,并返回对应的channel数据
go select原理
go select原理
假如没有channel可读或者可写,但是有default语句,则同上:返回default语句对应的scase并解锁所有的channel。
go select原理
假如既没有channel可读或者可写,也没有default语句,则将当前运行的groutine阻塞,并加入到当前所有channel等待队列中去。
go select原理
然后解锁所有channel,等待被唤醒。
此时如果有个channel可读或者可写ready了,则唤醒,并再次加锁所有channel,
go select原理
遍历所有channel找到那个对应的channel和G,唤醒G,并将没有成功的G从所有channel的等待队列中移除。
go select原理
如果对应的scase值不为空,则返回需要的值,并解锁所有channel
go select原理
如果对应的scase为空,则循环此过程。
select和channel之间的关系
go select原理

相关标签: 底层学习