> Golang 通过 `select...case` 语句实现了对 `channel` 的多路复用以及非阻塞收发操作
本文将着重讲解以下问题:
* 编译器对 `select` 语句的优化
* [`select` 如何随机选择 `case`](http://icebergu.com/archives/go-select-channel#%E5%A6%82%E4%BD%95%E9%9A%8F%E6%9C%BA%E9%80%89%E6%8B%A9-case)
* [当多个 `channel` 可以进行收发操作时,`select` 又会如何选择 `case`](http://icebergu.com/archives/go-select-channel#%E5%A4%9A%E4%B8%AAchannel%E5%90%8C%E6%97%B6%E5%87%86%E5%A4%87%E8%BF%9B%E8%A1%8C%E6%94%B6%E5%8F%91%E6%93%8D%E4%BD%9C%E5%A6%82%E6%9E%9C%E8%A7%A3%E5%86%B3%E7%AB%9E%E4%BA%89%E9%97%AE%E9%A2%98)
> **强烈建议阅读本文前,先看关于 channel 的源码解析 [Go 深入源码 —— Channel](http://icebergu.com/archives/go-channel)**
>
> 如果只关心 select 如何实现 channel 的多路复用,以及如何随机选择 `case` 的话可以直接看 [selectgo 实现 channel 的多路复用](http://icebergu.com/archives/go-select-channel#selectgo-%E5%AE%9E%E7%8E%B0-channel-%E7%9A%84%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8)
## select 使用与编译阶段的转换
### 无 case,永久阻塞
```go
select{}
```
没有任何 case 的 `select` 语句会被编译器转换为`runtime.block()`函数,永久阻塞
##### 编译阶段转换
```go
// src/cmd/compile/internal/gc/select.go
func walkselectcases(caes *Nodes) []*Node {
// 获取 case 操作的数量
n := cases.Len()
if n == 0 {
return []*Node{mkcall("block", nil, nil)}
}
// ...
}
// src/runtime/select.go
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) // 永久阻塞
}
```
### 单一 channel,阻塞收发
```go
ch := make(chan struct{})
select {
case data <- ch:
fmt.Printf("ch data: %v\n", data)
}
// 编译器转换
if ch == nil {
block()
}
data := <- ch
```
只有一个 channel 操作,实际会被编译器转换为相应channel 相应的收发操作,其实和实际调用 `data := <- ch` 并没有什么区别
##### 编译阶段转换
```go
// src/cmd/compile/internal/gc/select.go
func walkselectcases(caes *Nodes) []*Node {
// 获取 case 操作的数量
n := cases.Len()
// ...
if n == 1 {
if cas.Left != nil { // not default
n := cas.Left // n 表示 case 的 channel 操作
// ...
// 这一段操作便是转换成 if ch == nil {block()}; n;
a := nod(OIF, nil, nil)
a.Left = nod(OEQ, ch, nodnil())
var ln Nodes
ln.Set(l)
a.Nbody.Set1(mkcall("block", nil, &ln))
l = ln.Slice()
l = append(l, a, n)
}
// ...
return l
}
}
```
### 单一 channel + default,非阻塞收发
```go
ch := make(chan int)
select {
case ch <- 1:
// ...
default:
fmt.Println("do default")
}
// 编译器转换
if selectnbsend(ch, 1) {
// ...
} else {
fmt.Println("do default")
}
```
编译器会将 channel 的收发操作转换成 `selectnbsend` 或者 `selectnbrecv`/`slectnbrecv2`函数来完成非阻塞操作
##### 编译阶段转换
```go
// src/cmd/compile/internal/gc/select.go
func walkselectcases(caes *Nodes) []*Node {
// 获取 case 操作的数量
n := cases.Len()
// ...
// 两个 case操作,其中一个还是 default
if n == 2 && (cases.First().Left == nil || cases.Second().Left == nil {
n := cas.Left // n 是 case 的 channel 操作
r := nod(OIF, nil, nil)
switch n.Op {
case OSEND:
ch := n.Left
// 转换为 if selectnbsend(c, v){ //... } else { // default body ...}
r.Left = mkcall1(chanfn("selectnbsend", 2, ch.Type), types.Types[TBOOL], &r.Nint, ch, n.Right)
case OSELRECV:
// 转换为 if selectnbrecv(&v, c){ //... } else { // default body ...}
// ...
case OSELRECV2:
// 转换为 if selectnbrecv2(&v, &received, c){ //... } else { // default body ...}
// ...
}
return
}
}
```
channel 的非阻塞收发实际都是调用的 [`chansend`](http://icebergu.com/archives/go-channel#%E6%B7%B1%E5%85%A5%E6%BA%90%E7%A0%81) 或者 [`chanrecv`](http://icebergu.com/archives/go-channel#%E6%B7%B1%E5%85%A5%E6%BA%90%E7%A0%81-1)
```go
// src/runtime/chan.go
// 非阻塞发送
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
// block 参数为 false,非阻塞调用
return chansend(c,elem, false, getcallerpc())
}
// 非阻塞接收
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
selected, *received = chanrecv(c, elem, false)
return
}
```
而且 channel 会对于非阻塞收发操作有一些优化
```go
// 非阻塞操作, block 参数为 false
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ...
// 快速检测,非阻塞时,有些情况不需要获取锁就可以直接返回
// 非阻塞,未关闭,非缓冲+没有等待接收的 goroutine 或者 缓冲+缓冲区已满
if !block && c.closed == 0 &&
((c.dataqsiz == 0 && c.recvq.first == nil) ||
((c.dataqsiz < 0 && c.qcount == c.dataqsiz)) {
// 返回 false,表示未发送成功
return false
}
lock(&c.lock)
}
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool){
// ...
// 快速检测,在非阻塞模式下,和发送一样有些条件不需要加锁就可以直接判断返回
// 非阻塞并且未关闭,非缓冲+没有待发送者或者有缓冲+缓冲为空
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
lock(&c.lock)
}
```
在加锁前对 channel 进行判断,较少了加锁带来的性能问题
具体的 channel 收发操作,可以参考 [Go 深入源码 —— Channel](http://icebergu.com/archives/go-channel)
### 多 channel,实现多路复用
```go
ch1 := make(chan int)
ch1 := make(chan int)
select {
case data, ok := <- ch1:
fmt.Println("recv from ch1, data: %v, ok: %v", data, ok)
case ch2 <- 1:
fmt.Println("send to ch2")
}
// 编译器转换
// 第一阶段 生成 scase 对象数组
selv := [2]scase{}
order := [2*2]uint16
for i, case := range cases {
c := cases{}
c.kind = ...
c.c = ...
c.elem = ...
}
// 第二阶段 编译器生成调用 selectgo 的逻辑
chosen, recv := selectgo(selv, order, 2)
// 第三阶段 根据 selectgo 返回值 chosen 来生成 if 语句来执行相应索引的 case
if chosen == 0 {
// ...
}
if chosen == 1 {
// ...
}
```
多 case 的 `select` 语句实现了 channel 的多路复用,select 会阻塞,直到有 case 操作完成了收发操作
**如果有多个 case 操作已经可以执行收发操作了,但是 select 会随机选择一个 case 完成收到操作,然后执行 case 下的相应逻辑**
编译器实际会将 select 语句转换成 `selectgo` 函数,每个 case 操作都会被转换成 `scase` 结构,然后作为参数传递给 `selectgo`
##### 编译阶段转换
```go
// src/cmd/compile/internal/gc/select.go
func walkselectcases(cases *Nodes) []*Node {
// 获取 case 操作的数量
n := cases.Len()
if n == 0 { // 无 case
// ...
}
if n == 1 { // 单一 case
// ...
}
if n == 2 && (cases.First().Left == nil || cases.Second().Left == nil) { // 单一 case + defult
// ...
}
// 后续逻辑便是多 case 的情况了
// selv scase 数组,scasetype() 返回的便是 scase
// selv 和 order 会作为 selectgo 的参数
selv := temp(types.NewArray(scasetype(), int64(n)))
order := temp(types.NewArray(types.Types[TUINT16], 2*int64(n)))
// 第一阶段
// 遍历 case 生成 scase 对象存放到 selv 中
for i, cas := range cases.Slice() {
var c, elem *Node
var kind int64 = caseDefault
// cas.Left 如果不为 nil,那么说明是 channel 的收发操作
if n := cas.Left; n != nil {
switch n.Op {
case OSEND:
kind = caseSend
c = n.Left
elem = n.Right
case OSELRECV, OSELRECV2:
kind = caseRecv
c = n.Left
elem = n.Right
}
}
// 根据 c, elem 构造 scase,并存放到 selv 中
// ...
}
// selectgo 会返回的两个值,chosen 表示被选中的 case 的索引,recvOK 表示对于接收操作,是否成功接收
chosen := temp(types.Types[TINT])
recvOK := temp(types.Types[TBOOL])
// 第二阶段
// chose, recvOK := selectgo(selv, order, n)
r = nod(OAS2, nil, nil)
r.List.Set2(chose, recvOK)
fn := syslook("selectgo")
r.Rlist.Set1(mkcall1(fn, fn.Types, Results(), nil, bytePtrToIndex(selv, 0), bytePtrToIndex(order, 0), nodeintconst(int64(n))))
// 第三阶段
// 根据 selectgo 返回的 case 索引生成多个 if 语句
for i, cas := range cases.Slice() {
// 转换成if chosen == i { // ... case body }
cond := nod(OEQ, chosen, nodintconst(int64(i)))
r = node(OIF, cond, nil)
// ...
r.Nbody.AppendNodes(&cas.NBody)
}
}
```
**在分配 `selv` 和 order` 时,使用 `temp` 方法,应该是为了保证数据会分配到栈中而不是堆中**
### 多 channel + default
```go
ch1 := make(chan int)
ch1 := make(chan int)
select {
case data, ok := <- ch1:
fmt.Println("recv from ch1, data: %v, ok: %v", data, ok)
case ch2 <- 1:
fmt.Println("send to ch2")
default:
fmt.Println("do default")
}
```
多 case + default 同样也是调用 selectgo 函数,`default` 也会封装成 kind 为 caseDefault 的 `scase` 对象的
**`selectgo` 并不会进行阻塞,如果所有 case 的 channel 都无法立即完成收发操作,那就会直接执行 default 操作**
## selectgo 实现 channel 的多路复用
对于 select 语句,编译器会进行相应的转换优化操作,而多 channel 操作便是去调用 `selectgo` 函数来实现
### scase
select 中 case 操作,无论是 channel 收发还是 default 都会被转换为 `scase` 结构
```go
const (
caseNil = iota // 表示 channel 为 nil 的情况
caseRecv
caseSend
caseDefault
)
type scase struct {
c *hchan //进行收发操作的 channel
elem unsafe.Pointer // 收发的数据源
kind uint16 // 上述的四种 kind
...
}
```
scase 结构很简单,主要记录了 case 操作的类型,channel,以及 channel 收发的数据源
### selectgo
现在我们开始分析 select 的重头戏 `selectgo` 函数
```go
func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool)
```
* `cas0` 指向一个类型为 [ncases]scase 的数组
* `order0` 是一个指向[2*ncases]uint16,数组中的值都是 0
* `selectgo` 会返回选中的序号,如果是个接收操作,还会返回是否接收到一个值
如果看过[上文](http://icebergu.com/archives/go-select-channel#%E7%BC%96%E8%AF%91%E9%98%B6%E6%AE%B5%E8%BD%AC%E6%8D%A2-3)的话就会知道,编译器会将 scases 数组,还有用于排序的数组和 case 数量一起传给 selectgo
> 为什么 `selectgo` 还需要传递一个 order0,而不是直接根据 ncase 直接分配呢
> 编译转换会使用 `temp` 函数来构造生成数组的语句,而这个语句便可以保证数据会分配到栈上,而不是堆上,避免了不必要的堆分配
而 select 做的第一件事便是将 case0,order0 这些指针转换成相应的 slice 结构
```
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
// [:n:n] 的方式会让slice 的 len 和 cap 相等
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases]
// 对于 channel 为 nil 的收发操作,他们的 kind 被修改为 caseNil
for i := range scases {
case := &scases[i]
if cas.c == nil && cas.kind != caseDefault {
*cas = scase{}
}
}
```
order1 会被分为 `pollorder` 和 `lockorder`,而这两个slice将会真正决定 `select` 的随机选择以及死锁问题
##### 如何随机选择 case
**`selectgo` 是通过循环 `scases` 来挑选可以收发的 channel**
**然而循环时并不是按照 `scases`的顺序,而是 `pollorder` 中记录的顺序, 这样可以避免 channel 的饥饿问题**
**为了保证 `select` 随机选择 case,所以使用 `fastrandn` 来生成随机数**
```go
for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i+1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}
```
`pollorder` 在开始的时候值都是 0,循环结束后值便是随机顺序的 scases 索引
##### 避免相同 channel 重复加锁,以及死锁问题
**`selectgo` 在查找 scases 中已经可以进行收发操作的 channel 前会先对所有的 channel 进行加锁操作**
###### 死锁问题
> 如果多个 goroutine 都需要锁定 ch1 ch2,而他们加锁的顺序不固定,那么很可能会出现死锁问题
> 这个时候,对加锁的顺序就有要求了,按照同样的顺序的话,没有竞争到 ch1.lock 的 goroutine,会等待加锁 ch1.lcok,而不会直接去加锁 ch2.lock
**加锁前首先会对 `lockorder` 进行堆排序,生成由 case.c(\*hchan) 来排序的 scases 索引顺序**
```go
func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) {
...
// ... 对 looporder 堆排序
// selectgo 在查找 scases 前,先对所有 channel 加锁
sellock(scases, lockorder)
...
}
```
**`sellock` 对地址相同的 channel 只会加锁一次**
```go
func sellock(scases []scases, lockorder []int16) {
var c *hchan
for _, o := range lockorder {
c0 := scases[0].c // 根据加锁顺序获取 case
// c 记录了上次加锁的 hchan 地址,如果和当前 *hchan 相同,那么就不会再次加锁
if c0 != nil && c0 != c {
c = c0
lock(&c.lock)
}
}
}
```
加锁完成后,可以进入 `selectgo` 主循环逻辑了
主逻辑会分为三部分:
1. 首先根据 `pollorder` 的顺序查找 scases 是否有可以立即收发的 channel
2. channel 都没有准备好,并且不存在 default,那么就将当前 `goroutine` 加入到 channel 相应的等待队列,然后等待收其他 `goroutine` 唤醒
3. 被唤醒后,再次找到满足条件的 channel
##### 查找 scases 中是否有准备好的 channel
```go
loop:
for i := 0; i < ncases; i++ {
// 根据 `pollorder` 记录的随机 scases 索引来获取 cas
casi = int(pollorder[i])
cas = &scases[casi]
c = cas.c
switch case.kind {
case caseNil:
continue
case caseRecv:
// ...
case caseSend:
// ...
case caseDefault:
// ...
}
}
// ...
}
```
根据 `pollorder` 记录的随机 `scases` 索引来遍历处理 case,然后根据 `case.kind` 来查看 channel 是否准备好,然后 `goto` 跳转到相应逻辑
`case.kind` 为 `caseNil`,说明 channel 为 nil,那么 `continue`,不进行任何处理
###### caseRecv: channel 接收操作
```go
switch case.kind {
case caseRecv:
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
if c.qcount > 0 {
goto bufrecv
}
if c.closed != 0 {
goto rclose
}
...
}
```
如果 channel 中有待发送的 goroutine, 跳转到 `recv`,调用 [`recv`](https://github.com/golang/go/blob/master/src/runtime/chan.go#L602)完成接收操作
```go
recv:
// src/runtime/chan.go
recv(c, sg, cas.elem, func() { selunlock(cases, lockorder) }, 2)
recvOK = true
goto retc
```
如果 channel 中有缓冲数据,那么跳转到 `bufrecv`,从缓冲区中获取数据
```go
bufrecv:
recvOK = true
gp = chanbuf(c, c.recx)
if cas.elem != nil {
typedmemclr(c.elemtype, gp)
}
// ... channel 缓冲区调整
selunlock(scases, lockorder)
goto retc
```
如果 channel 已关闭,跳转到 `rclose`, 将接收值置为空值,recvOK 置为 false
```go
rclose:
selunlock(scases, lockorder)
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
goto retc
```
接收操作与正常接收操作类似,可以参考 [channel 接收数据](http://icebergu.com/archives/go-channel#%E6%B7%B1%E5%85%A5%E6%BA%90%E7%A0%81-1)
###### caseSend: channel 发送操作
```go
switch case.kind {
case caseSend:
if c.closed != 0 {
goto sclose
}
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
if c.qcount < c.dataqsiz {
goto bufsend
}
...
}
```
对于发送操作会先判断 channel 是否已经关闭,跳转到 `sclose`,直接 panic
```go
sclose:
selunlock(scases, lockorder)
panic(plainError("send on closed channel"))
```
如果 channel 为关闭,并且有待接收队列不为空,说明 channel 的缓冲区为空,跳转到 send , 调用 `send` 函数,直接发送数据给待接收者
```go
send:
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
goto retc
```
如果缓冲区不为空的话,跳转到 `bufsend`,从缓冲区获取数据
```go
bufsend:
typedmemove(c.elemtype, chanbuf(c, c.sendx), cas.elem)
// ... 调整缓冲区索引
selunlock(scases, lockorder)
goto retc
```
###### caseDefult: default
```go
var dfli int
var dfl *scase
for i := 0; i < ncases; i++{
// ...
switch case.kind {
...
case caseDefault:
dfli = casi
dfl = cas
}
}
if dfl != nil {
selunlock(scases, lockorder)
casi = dfli
cas = dfl
return casi, recvOK
}
```
`dfli` 和 `dfl` 记录了 kind 为 `caseDefault` 的 case
如果所有 channel 都没有完成收发操作,那么就不会执行 `goto` 跳转,循环结束后判断 `dfl != nil` 存在 default,就直接返回 default 的索引 `dfli`
##### 加入 channel 等待队列,等待被唤醒
如果没有 channel 可以执行收发操作,并且没有 `default case`,那么就将当前 `goroutine` 加入到 channel 相应的收发队列中,等待被其他 goroutine 唤醒
```go
func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) {
// ...
setlock(scases, lockorder)
// ... 查看是否有准备好的 channel,或者存在 default case
gp = getg()
for _, casi := range lockorder {
casi = int(casei)
cas = &scases[casi]
if cas.kind == caseNil {
continue // channel 为 nil 直接跳过
}
c = cas.c
// 构造 sudog
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
sg.elem = cas.elem
sg.c = c
// 加入相应等待队列
switch cas.kind {
case caseRecv:
c.recvq.enqueue(sg)
case caseSend:
c.sendq.enqueue(sg)
}
}
// 被唤醒后会根据 param 来判断是否是由 close 操作唤醒的,所以先置为 nil
gp.param = nil
// selparkcommit 会解锁所有 channel
gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1)
```
将当前 goroutine 加入到每一个 channel 等待队列中
**如果所有的 channel 都是 nil 的话就会被永久阻塞,不会被唤醒**
###### 多个channel同时准备进行收发操作,如果解决竞争问题
**`selectgo` 构建出来的 `sudog` 会将 `isSelect` 置为 true,这样时为了避免多个 channel 从等待队列中获取相同 goroutine 封装的 `sudog`**
现在来看一下 等待队列的出队方法 `dequeue`
```go
struct hchan {
...
sendq *waitq
recvq *waitq
}
func (q *waitq) dequeue() *sudog {
for {
sgp := q.first
if sgp == nil {
return nil
}
// ...
// 原子操作,如果 spg.g.selectDone 不为 1,则修改为 0
// 为 1 说明 spg.g 已经被其他 channel 取出,直接跳过 sgp
if sgp.isSelect && !atomic.Cas(spg.g.selectDone, 0, 1) {
continue
}
}
return sgp
}
```
##### 被唤醒,选择合适的 case
```
func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) {
// ...
gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1)
// 加锁所有的 channel
sellock(scases, lockorder)
// param 存放唤醒 goroutine 的 sudog,如果是关闭操作唤醒的,那么就为 nil
sg = (*sudog)(pg.param)
gp.param = nil
// waiting 链表按照 lockorder 顺序存放着 sudog
sglist = gp.waiting
casi = -1
cas = nil // cas 便是唤醒 goroutine 的 case
for _, casei := range lockorder {
k = &scases[casei]
if k.kind == caseNil {
continue
}
// 如果相等说明,goroutine 是被当前 case 的 channel 收发操作唤醒的
// 如果是关闭操作,那么 sg 为 nil, 不会对 cas 赋值
if sg == sglist {
casi = int(casei)
cas = k
} else {
// goroutine 已经被唤醒,将 sudog 从相应的收发队列中移除
c = k.c
// func (q *waitq) dequeueSudoG(sgp *sudog)
// dequeueSudoG 会通过 sudog.prev 和 sudog.next 将 sudog 从等待队列中移除
if k.kind == caseSend {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
// 释放 sudog,然后准备处理下一个 sudog
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}
...
}
```
当前`goroutine` 被唤醒后,将其他 `sudog` 从相应的 channel 等待队列中移除
`selectgo` 会根据变量 `cas` 的值来判断是收发操作唤醒还是关闭操作唤醒
关闭操作唤醒的话 `gp.param` 会被置为 nil,那么就不会赋值 `cas` 变量
```
func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) {
// ...
loop:
// ... 查找 scases 中是否有准备完成的 channel
gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1)
// 加锁所有的 channel
sellock(scases, lockorder)
// ...
if cas == nil {
// 由关闭操作唤醒 goroutine,那么再次回到 loop 处
goto loop
}
c = cas.c
if cas.kind == caseRecv {
recvOK = true
}
selunlock(scases, lockorder)
return casi, recvOK
}
```
###### 关闭操作唤醒 selectgo,并不一定会选择该 case
对于关闭操作唤醒,逻辑会回到 `loop` 中再次执行 scases 的检查操作
**关闭操作唤醒 `selectgo` 后,`在完成所有 channel 加锁前又有 channel 准备好收发操作了`,那么在 `loop` 查询时,按照 `pollorder` 随机顺序,可能会选中刚刚准备好的 channel,而不是唤醒 `selectgo` 的 case**
###### 收发操作唤醒 selectgo, 必定会选择该 case
**而对于收发操作,已经完成了值的拷贝,必然会选择这个 case,而不会再次去查询**
## 总结
**使用 `单 channel`,`单 channel + default` 时编译器会对 select 进行编译转换,这些转换会带来性能的优化**
`多 channel: `
* **首先会对所有的所有的 `channel` 进行加锁,而加锁过程会按照 `channel` 进行排序,避免了死锁的出现,并且不会对 `channel` 重复加锁**
* **加锁完成后,会使用随机数来打乱查询的顺序,随机选择 case 避免 `channel` 饥饿,保证公平性**
* **如果所有 `channel` 都没有准备好,并且有 `default case`,那么就选择 `default case`**
* **如果没有 `default`,那么 `select` 就会阻塞,然后等待被其他 goroutine 唤醒 select**
* **关闭操作唤醒 `select` 时,最终选择的并不一定是该 `channel` case,因为会再次使用随机顺序来选择合适的 `channel`**
* **收发操作唤醒 `select` 时,必然会选择该 channel case**
## 参考
[Go 语言设计与实现 - select](https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/)
## 推荐阅读
[Golang 夜读 第 56 期 channel & select 源码分析](https://github.com/talkgo/night/issues/450)
Go 深入源码 —— select 与 channel