Golang select 用法与实现原理

文章目录

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

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:

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)。

// 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 函数做了什么呢?

  1. 打乱传入的 case 结构体顺序。
  2. 锁定 scase 语句中所有的 channel。
  3. 按照随机顺序检测 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)
  4. 所有case都未ready,且没有default语句
    4.1 将当前协程 G 加入到所有 channel 的等待队列
    4.2 解锁所有 channel
    4.3 当将协程转入阻塞,等待被唤醒
  5. channel 可读或可写 ready 了,则唤醒。唤醒后返回 channel 对应的 case index
    5.1 如果是读操作,解锁所有的channel,然后返回(case index, true)
    5.2 如果是写操作,解锁所有的channel,然后返回(case index, false)

其中被阻塞的 G 由 runtime.sudog 来表示。

4.小结

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


参考文献

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

你可能感兴趣的:(Go,golang,select)