Golang select 用法与实现原理

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.基本语法

select {
    case <-channel1:
        // 处理 channel1 上的数据
    case data := <-channel2:
        // 处理 channel2 上的数据
    case channel3 <- data:
        // 将数据写入 channel3
    default:
        // 没有任何 channel 可用
}

select 语句会等待多个通道中的数据,一旦某个通道上有数据可读或可写,就会执行相应的 case 子句。

如果多个 case 子句同时满足条件,则随机选择其中一个执行。

如果没有任何 case 子句满足条件,则执行 default 子句。

如果没有 default 子句,则 select 会一直阻塞,直到有 channel 可用。

注意,select 读操作要判断是否成功读取,因为关闭的 channel 也可以读取,此时 ok 为 false。

case elem, ok := <-chan1:

3.实现原理

3.1 概述

select 语句是基于 Golang 运行时的调度器实现的 IO 多路复用。可以同时监控多个通道的状态,并在某个通道就绪时将其对应的 case 子句加入调度队列中等待执行。当某个 case 子句执行完毕后,select 语句就会结束,并返回对应的结果。

Golang 运行时调度器是一种基于 goroutine 的协作式调度机制,它能够在多个 goroutine 之间进行高效的上下文切换,从而实现并发执行。在调度器的实现中,每个 goroutine 会绑定到一个系统线程上,而系统线程则会在操作系统层面上执行调度,以实现多线程并发。调度器会监控每个 goroutine 的状态,并在 goroutine 处于阻塞状态时,将其从系统线程上解绑,然后将系统线程用于执行其他 goroutine,从而避免了阻塞操作对整个程序的影响。

在 Golang 中,使用 select 语句可以轻松地实现 IO 多路复用。当 select 语句被执行时,运行时调度器会将所有 case 子句中的通道加入到一个调度器队列中,并监控这些通道的状态。当有数据可读或可写时,调度器就会选择其中一个 case 子句,并将其对应的代码块加入到调度队列中等待执行。

3.2 数据结构

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 的数据存放地址。

3.3 实现原理

编译优化

首先在编译期间,Go 语言会对 select 语句进行优化,它会根据 select 中 case 的不同选择不同的优化路径:

  1. 不存在任何 case。

空的 select 语句会被转换成调用 runtime.block() 函数,直接挂起当前 Goroutine。

  1. 只存在一个 case。

如果 select 语句中只包含一个 case,编译器会将其转换成普通的通信操作,而不是一个真正的 select 。

  1. 多个 case 其中有一个是 default。

如果 select 语句中只包含多个 case 并且其中一个是 default,那么会使用 runtime.selectnbrecv()runtime.selectnbsend() 函数非阻塞地执行收发操作。

  1. 多个 case 无 default。

编译器会使用如下的流程处理 select 语句:

  • 将所有的 case 转换成包含 channel 以及类型等信息的 runtime.scase 结构体。
  • 调用运行时函数 runtime.selectgo() 从多个准备就绪的 channel 中选择一个可执行的 runtime.scase 结构体。
  • 生成一组 if 语句,在语句中判断自己是不是被选中的 case,然后执行 case 对应的代码块。

一个包含三个 case 的正常 select 语句其实会被展开成如下所示的逻辑:

selv := [3]scase{}
order := [6]uint16
for i, cas := range cases {
    c := scase{}
    c.kind = ...
    c.elem = ...
    c.c = ...
}
chosen, revcOK := selectgo(selv, order, 3)
if chosen == 0 {
    ...
    break
}
if chosen == 1 {
    ...
    break
}
if chosen == 2 {
    ...
    break
}

运行时

在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 runtime.selectgo() 函数,该函数的主要作用是用于选择就绪的 case。

源码 runtime.selectgo()(src/runtime/select.go)函数实现了 select 语句。

// 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 执行流程如下:

  1. 初始化

打乱传入的 case 结构体顺序。随机生成一个遍历的轮询顺序 pollOrder,并根据 Channel 地址生成锁定顺序 lockOrder。

  1. 循环

根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel。

  • 如果存在,直接获取 case 对应的索引并返回。
  • 如果不存在,创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒。
  1. 唤醒

当调度器唤醒当前 Goroutine 时,会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 对应的索引并返回。

其中被阻塞放到等待列表中的 G 由 runtime.sudog 来表示。

4.小结

总之,Golang 的 select 语句是一种基于运行时调度器实现的高效 IO 多路复用技术,可以轻松地实现多路复用和并发操作,从而提高程序效率和性能。


参考文献

OpenAI ChatGPT
Go 语言select 的实现原理 - 面向信仰编程
图解Go select语句原理 - 菜刚RyuGou的博客
Go select的使用和实现原理 - 博客园

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值