文章目录
- 1.简介
- 2.基本语法
- 3.实现原理
- 概述
- 数据结构
- 实现逻辑
- 4.小结
- 参考文献
1.简介
Golang 中的 select 语句是用于多路复用的一种语言结构,用于同时等待多个通道上的数据,并执行相应的代码块。
也就是说 select 是用来监听和 channel 有关的 IO 操作,它与 select,poll,epoll 相似,当 IO 操作发生时,触发相应的动作,实现 IO 多路复用。
特性如下:
- case 必须是一个通信操作。
- select 语句中除 default 外,各 case 执行顺序是随机的。
- select 语句中如果没有 default 语句,则会阻塞等待任意一个 case。
- select 语句中除 default 外,每个 case 只能操作一个 channel,要么读要么写。
- 当 select 中的多个 case 同时被触发时,会随机执行其中的一个。
2.基本语法
代码语言:javascript复制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。
代码语言:javascript复制case elem, ok := <-chan1:
3.实现原理
概述
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)。
代码语言:javascript复制// 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 的函数:
代码语言:javascript复制// 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 函数做了什么呢?
- 打乱传入的 case 结构体顺序。
- 锁定 scase 语句中所有的 channel。
- 按照随机顺序检测 scase 中的 channel 是否 ready: 3.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true) 3.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false) 3.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false)
- 所有case都未ready,且没有default语句 4.1 将当前协程 G 加入到所有 channel 的等待队列 4.2 解锁所有 channel 4.3 当将协程转入阻塞,等待被唤醒
- channel 可读或可写 ready 了,则唤醒。唤醒后返回 channel 对应的 case index 5.1 如果是读操作,解锁所有的channel,然后返回(case index, true) 5.2 如果是写操作,解锁所有的channel,然后返回(case index, false)
其中被阻塞的 G 由 runtime.sudog 来表示。
4.小结
总之,Golang 的 select 语句是一种基于运行时调度器实现的高效 IO 多路复用技术,可以轻松地实现多路复用和并发操作,从而提高程序效率和性能。