Golang 中的 select 语句是用于多路复用的一种语言结构,用于同时等待多个通道上的数据,并执行相应的代码块。
也就是说 select 是用来监听和 channel 有关的 IO 操作,它与 select,poll,epoll 相似,当 IO 操作发生时,触发相应的动作,实现 IO 多路复用。
特性如下:
select {
case <-channel1:
// 处理 channel1 上的数据
case data := <-channel2:
// 处理 channel2 上的数据
case channel3 <- data:
// 将数据写入 channel3
default:
// 没有任何 channel 可用
}
select 语句会等待多个通道中的数据,一旦某个通道上有数据可读或可写,就会执行相应的 case 子句。如果多个 case 子句同时满足条件,则随机选择其中一个执行。如果没有任何 case 子句满足条件,则执行 default 子句。如果没有 default 子句,则 select 会一直阻塞,直到有通道可用。
注意,select 语句中读操作要判断是否成功读取,因为关闭的 channel 也可以读取,此时 ok 为 false。
case elem, ok := <-chan1:
select 语句是基于 Golang 运行时的调度器实现的 IO 多路复用。可以同时监控多个通道的状态,并在某个通道就绪时将其对应的 case 子句加入调度队列中等待执行。当某个 case 子句执行完毕后,select 语句就会结束,并返回对应的结果。
Golang 的运行时调度器是一种基于 goroutine 的协作式调度机制,它能够在多个 goroutine 之间进行高效的上下文切换,从而实现并发和并行执行。在调度器的实现中,每个 goroutine 会绑定到一个线程上,而线程则会在操作系统层面上执行调度,以实现多线程并发。调度器会监控每个 goroutine 的状态,并在 goroutine 处于阻塞状态时,将其从线程上解绑,然后将线程用于执行其他 goroutine,从而避免了阻塞操作对整个程序的影响。
在 Golang 中,使用 select 语句可以轻松地实现 IO 多路复用。当 select 语句被执行时,运行时调度器会将所有 case 子句中的通道加入到一个调度器队列中,并监控这些通道的状态。当有数据可读或可写时,调度器就会选择其中一个 case 子句,并将其对应的代码块加入到调度队列中等待执行。
Golang 实现 select 时,并没有一个数据结构表示 select,但是有一个数据结构表示 case 语句(含 defaut,default 实际上是一种特殊的 case)。
select 执行过程可以类比成一个函数,函数输入case 数组,输出选中的 case,然后程序流程转到选中的 case 块
我们先看一下 case 的数据结构(go 1.19 runtime/select.go)。
// Select case descriptor.
// Known to compiler.
// Changes here must also be made in src/cmd/compile/internal/walk/select.go's scasetype.
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}
因为 case 中都与 Channel 的发送和接收有关,所以 runtime.scase 结构体中也包含一个 runtime.hchan 类型的字段存储 case 中使用的 Channel。
elem 表示缓冲区地址,表示从 Channel 读出的数据存放地址或将要写入 Channel 的数据存放地址。
源码 runtime.selectgo()(src/runtime/select.go)定义了 select 选择 case 的函数:
// selectgo implements the select statement.
//
// cas0 points to an array of type [ncases]scase, and order0 points to
// an array of type [2*ncases]uint16 where ncases must be <= 65536.
// Both reside on the goroutine's stack (regardless of any escaping in
// selectgo).
//
// For race detector builds, pc0 points to an array of type
// [ncases]uintptr (also on the stack); for other builds, it's set to
// nil.
//
// selectgo returns the index of the chosen scase, which matches the
// ordinal position of its respective select{recv,send,default} call.
// Also, if the chosen scase was a receive operation, it reports whether
// a value was received.
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool)
函数返回值:
int: 选中 case 的编号,这个 case 编号跟代码一致。
bool: 是否成功从channle中读取了数据,如果选中的case是从channel中读数据,则该返回值表示是否读取成功。
selectgo 函数做了什么呢?
其中被阻塞的 G 由 runtime.sudog 来表示。
总之,Golang 的 select 语句是一种基于运行时调度器实现的高效 IO 多路复用技术,可以轻松地实现多路复用和并发操作,从而提高程序效率和性能。
OpenAI ChatGPT
Go 语言select 的实现原理 - 面向信仰编程
图解Go select语句原理 - 菜刚RyuGou的博客
Go select的使用和实现原理 - 博客园