深入浅出Golang中select的实现原理
概述
在go语言中,select
语句就是用来监听和channel
有关的IO
操作,当IO
操作发生时,触发相应的case
操作,有了select
语句,可以实现main
主线程与goroutine
线程之间的互动。需要的朋友可以参考以下内容,希望对大家有帮助。
select实现原理
Golang
实现select
时,定义了一个数据结构表示每个case
语句(包含default
,default
实际上是一种特殊的case
),select
执行过程可以看成一个函数,函数输入case
数组,输出选中的case
,然后程序流程转到选中的case
块。
执行流程
在默认的情况下,select 语句会在编译阶段经过如下过程的处理:
- 将所有的
case
转换成包含Channel
以及类型等信息的 scase 结构体; - 调用运行时函数
selectgo
获取被选择的scase
结构体索引,如果当前的scase
是一个接收数据的操作,还会返回一个指示当前case
是否是接收的布尔值; - 通过
for
循环生成一组if
语句,在语句中判断自己是不是被选中的case
。
case数据结构
select
控制结构中case
使用了scase
结构体来表示,源码包class="lazy" data-src/runtime/select.go:scase
定义了表示case
语句的数据结构:
type scase struct {
c *hchan
elem unsafe.Pointer
kind uint16
pc uintptr
releasetime int64
}
scase.c:由于非default
的case
中都与channel
的发送和接收数据有关,所以在scase
结构体中也包含一个c
字段用于存储case
中使用的channel
,为当前case
语句所操作的channel
指针,这也说明了一个case
语句只能操作一个channel
。
scase.kind:表示该case的类型,分为读channel
、写channel
和default
,三种类型分别由常量定义:
const (
caseNil = iota
caseRecv //case语句中尝试读取scase.c中的数据;
caseSend //case语句中尝试向scase.c中写入数据;
caseDefault //default语句
)
scase.elem:用于接收或者发送数据的变量地址,根据scase.kind
不同,有不同的用途:
- scase.kind == caseRecv :
scase.elem
表示读出channel
的数据存放地址; - scase.kind == caseSend :
scase.elem
表示将要写入channel
的数据存放地址;
执行select
在运行期间会调用selectgo()
函数,这个函数主要作用是从select
控制结构中的多个case
中选择一个需要执行的case
,随后的多个 if
条件语句就会根据 selectgo()
的返回值执行相应的语句。
运行时源码包class="lazy" data-src/runtime/select.go:selectgo()
定义了select选择case的函数:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]
for i := range scases {
cas := &scases[i]
if cas.c == nil && cas.kind != caseDefault {
*cas = scase{}
}
}
for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i + 1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}
// sort the cases by Hchan address to get the locking order.
// ...
sellock(scases, lockorder)
// ...
}
selectgo
函数首先会进行执行一些初始化操作,也就是决定处理 case
的两个顺序,其中一个是 pollOrder
另一个是 lockOrder
。
函数参数:
- cas0:为scase数组的首地址,
selectgo()
就是从这些scase
中找出一个返回。 - order0:为一个两倍cas0数组长度的buffer,保存scase随机序列
pollorder
和scase
中channel
地址序列lockorder
; - pollorder:每次
selectgo
执行都会把scase
序列打乱,以达到随机检测case
的目的。 - lockorder:所有
case
语句中channel
序列,以达到去重防止对channel
加锁时重复加锁的目的。 - ncases:表示
scase
数组的长度
函数返回值:
- int: 选中
case
的编号,这个case
编号跟代码一致 - bool: 是否成功从
channle
中读取了数据,如果选中的case
是从channel
中读数据,则该返回值表示是否读取成功。
循环
当 select 语句确定了轮询和锁定的顺序并锁定了所有的 Channel
之后就会开始进入 select
的主循环,查找或者等待 Channel
准备就绪,循环中会遍历所有的 case
并找到需要被唤起的sudog
结构体。
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
// ...
gp = getg()
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
if cas.kind == caseNil {
continue
}
c = cas.c
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
sg.elem = cas.elem
sg.c = c
*nextp = sg
nextp = &sg.waitlink
switch cas.kind {
case caseRecv:
c.recvq.enqueue(sg)
case caseSend:
c.sendq.enqueue(sg)
}
}
gp.param = nil
gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
// ...
}
在这段循环的代码中,我们会分四种不同的情况处理 select
中的多个 case
:
caseNil — 当前 case
不包含任何的 Channel
,就直接会被跳过;
caseRecv — 当前 case
会从 Channel
中接收数据;
- 如果当前
Channel
的sendq
上有等待的Goroutine
就会直接跳到recv
标签所在的代码段,从Goroutine
中获取最新发送的数据; - 如果当前
Channel
的缓冲区不为空就会跳到bufrecv
标签处从缓冲区中获取数据; - 如果当前
Channel
已经被关闭就会跳到rclose
做一些清除的收尾工作;
caseSend — 当前 case
会向 Channel
发送数据;
- 如果当前
Channel
已经被关闭就会直接跳到rclose
代码段; - 如果当前
Channel
的recvq
上有等待的Goroutine
就会跳到send
代码段向Channel
直接发送数据;
caseDefault — 表示默认情况,如果循环执行到了这种情况就表示前面的所有case
都没有被执行,所以这里会直接解锁所有的 Channel
并退出 selectgo
函数,这时也就意味着当前 select
结构中的其他收发语句都是非阻塞的。
总结
通过以上内容我们简单的了解了select
结构的执行过程与实现原理,首先在编译期间,Go
语言会对 select
语句进行优化, 对于空的select
语句会直接转换成block
函数的调用,直接挂起当前Goroutine
,如果select
语句中只包含一个case
,就会被转换成if ch == nil {block}; n;
表达式。然后执行case结构体中内容。
在运行时会执行selectgo
函数,随机生成一个遍历的轮询顺序pollOrder
并根据Channel
地址生成一个用于遍历的锁定顺序lockOrder
;然后根据pollOrder
遍历所有的case
查看是否有可以处理的Channel
消息,如果有消息就直接获取case对应的索引并返回。如果没有消息就会创建sudog结构体,将当前 Goroutine
加入到所有相关 Channel
的sendq
和 recvq
队列中并调用 gopark
触发调度器的调度;
注意: 并不是所有的select控制结构都会走到selectgo
,很多情况都会被直接优化调。
以上就是深入浅出Golang中select的实现原理的详细内容,更多关于Golang select原理的资料请关注编程网其它相关文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341