概述

在 Go 里面,有个很常用的 select 语句,用平时都会用,但是你知道它底层的实现机制吗?我不知道,所以我就学习了一下,顺便记录一下结论。

channel 的工作机制

在开始说 select 之前,先来收一下 unbuffer 的 channel 是怎么工作的,其实 buffer channel 在 buffer 满的时候也是类似的机制,那就先说 unbuffer 吧。

在 channel 的内部,有两个数据结构,分别是 sendqrecvq,他们用于放置因为发送消息到 channel 或者从 channel 中读取消息而阻塞的 goroutine;当然,对于发送消息而阻塞的 goroutine 除了存放 goroutine 指针之外,还会存放发送的消息指针。

如果一个有阻塞的 recv goroutine 遇到一个发送来的消息的时候,channel 就会从 recvq 中出队一个接收 goroutine,然后通过 memmove 将消息 copy 给他,从而完成一个 channel 的消息交换,内部数据结构类似于:

对于 buffer channel,其实是多了一个 buffer 的存储,在 Go 里面,buffer 的存储是类似于一个环状队列实现,在 channel 结构中有一下属性:

通过这几个变量,就可以实现环形数组了。

select 的实现

当了解完 channel 的实现之后,再来理解 select 就比较容易一些了,例如这一段代码:

  1. [[email protected].io]# cat main.go
  2. select {
  3. case <- a:
  4. case <- b:
  5. case <- c:
  6. }

当 a、b 和 c channel 都阻塞的时候,在 Go 的实现里面,其实就是在 a、b 和 c 三个 channel 的等待队列里面都将 select 这个 goroutine 注册上,当这三个中只要有一个返回了之后,select 对应的 goroutine 就会得到激活并运行,只不过 Go 需要额外地完成其他未激活 channel 的出队列操作。

但是,Go 在某些条件下,会简化问题,例如下面这段带 default 的代码:

  1. [[email protected].io]# cat default.go
  2. select {
  3. case <- a:
  4. default:
  5. }

如果 channel a 是阻塞的,其实 select 就不用往 channel 的等待队列中插入 goroutine 了,因为这个时候直接执行 default 就可以了,所以 Go 的简化逻辑就是:对于这种带 default 的 select,直接检查对应的 channel 是否有数据,如果没有直接执行 default,有数据,直接获取数据,并运行对应的代码。

还有一种更简单的逻辑,Go 的简化也不一样:

  1. [[email protected].io]# cat single.go
  2. select {
  3. case <- a:
  4. }

对于这种只有一个 channel 的,其实就是我们平时使用 channel 的简单方式,所以可以直接简化成:

  1. [[email protected].io]# cat single2.go
  2. <- a

Ref