引言
在Golang中,select
是专门为channel
设计的关键词,select
结合 channel
实现了多路复用模型。
如果你对channel
还不够了解,强烈建议你先看这篇文章 从源码分析Channel
本文基于 go1.16.17
简介
select
类似Java的switch
,不同的是,Golang中的select
是专为channel
而设计的。select
主要用于监听多个channel
是否可以收发消息,select会尝试执行case语句
。当任何一个case满足条件则会执行,若没有可执行的case,就会执行default分支。如果default也不满足,程序会跳出select语句块.
使用
先来一段简单的,看看程序会输出什么
func tmp1() {
ch := make(chan int, 1)
ch <- 1 // 这里需要注意,如果读一个有缓冲且无数据的channel 会panic。因此先推一条数据进入channel
select {
case data, ok := <-ch:
if ok {
log.Printf("recv-data:[%v]", data)
} else {
log.Println("channel-closed!")
}
default:
log.Println("into-default")
}
log.Println("return")
}
复制代码
不出意外,程序从channel
中读出一条数据,跳出了select
语句块
那么我想一直监听select语句块怎么办呢 ?用for循环就好了!
func tmp1() {
ch := make(chan int, 1)
ch <- 1
for{
select {
case data, ok := <-ch:
if ok {
log.Printf("recv-data:[%v]", data)
} else {
log.Println("channel-closed!")
}
}
}
log.Println("return")
}
复制代码
select
中的case执行了一次,随后由于 尝试从有缓冲区且无数据的channel读数据,程序panic了
这里需要注意,如果其他case一直不能被满足,那么不能有default
分支,否则会发生cpu空转
每个select
只能监听一个case
么?当然不是。那么case的执行顺序是怎么样的呢?我们写一段代码来看看
代码新建了4个channel ,分别往channel里推1w条数据,随后执行select 统计每个case执行的频率
func tmp3() {
countMap := make(map[int]int, 0)
mu := sync.Mutex{}
ch1 := make(chan int, 100000)
ch2 := make(chan int, 100000)
ch3 := make(chan int, 100000)
ch4 := make(chan int, 100000)
wg := &sync.WaitGroup{}
wg.Add(4)
go func() {
defer wg.Done()
write(ch1, 1)
}()
go func() {
defer wg.Done()
write(ch2, 2)
}()
go func() {
defer wg.Done()
write(ch3, 3)
}()
go func() {
defer wg.Done()
write(ch4, 4)
}()
wg.Wait()
time.Sleep(time.Second * 2)
for i := 0; i < 10000; i++ {
select {
case data, ok := <-ch1:
if ok {
mu.Lock()
countMap[data]++
mu.Unlock()
} else {
log.Println("ch1-channel-closed!")
}
case data, ok := <-ch2:
if ok {
mu.Lock()
countMap[data]++
mu.Unlock()
} else {
log.Println("ch2-channel-closed!")
}
case data, ok := <-ch3:
if ok {
mu.Lock()
countMap[data]++
mu.Unlock()
} else {
log.Println("ch3-channel-closed!")
}
case data, ok := <-ch4:
if ok {
mu.Lock()
countMap[data]++
mu.Unlock()
} else {
log.Println("ch4-channel-closed!")
}
}
}
log.Println(countMap)
}
func write(ch chan int, num int) {
for i := 0; i < 10001; i++ {
ch <- num
}
}
复制代码
最后看一下我们的统计:
可以看到,我们每个case执行频率基本相同,下面让我们一起看看select
底层是如何实现的
数据结构
select
在底层不存在单独的结构体,我们使用runtime.scase
表示select
中的 case
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}
复制代码
可以看到,每个case块
持有一个chan指针
, default
是一个特殊的scase
,用retc
表示
重点函数分析
准备阶段
在准备阶段,有三种情况时,编译器会优化:
- 没有case和default情况下,直接调用gopark()使当前协程永远阻塞 。
- 如果只有一个case,那么select会被优化成一个if语句.
- 只有一个case和default.
- 如果这个case是发送操作,会直接调用channel.chansend()进行一次非阻塞的发送
- 如果是接收操作,会直接调用chan.chanrecv()进行一次非阻塞的接收操作
执行过程
select
执行过程位于 runtime.selectgo
方法中。我们来看一下
方法运行过程主要分为4个阶段
- 按照规则生成遍历顺序 pollorder && lockorder
- 按pollorder顺序尝试channel是否ready
- 如果 (2)都不满足
3.1 如果有default语句 ,执行default语句后返回
3.2 如果没有default语句 ,阻塞当前协程,将当前协程压入所有channel的等待队列&&接收队列中,等待唤醒 - 被唤醒后,按lockorder顺序解锁scases,从其他channel中将本sudog出队,返回 caseIndex
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
// 为了保持堆栈大小,scase 的数量上限为 65536。
// 即 每个select最多有65536个case语句
// 计算pollorder && lockorder ,他们的的底层数组未由编译器进行零初始化
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
ncases := nsends + nrecvs
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]
// dosomething
// 阶段1 生成pollorder && lockorder 遍历顺序
// pollorder:随机顺序
norder := 0
for i := range scases {
cas := &scases[i
if cas.c == nil {
cas.elem = nil // allow GC
continue
}
// 类似洗牌算法
j := fastrandn(uint32(norder + 1))
pollorder[norder] = pollorder[j]
pollorder[j] = uint16(i)
norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]
// lockorder:按chan在堆上的地址进行堆排序生成的顺序
// 省略堆排序过程
// 按照lockorder顺序对scases内存加锁
sellock(scases, lockorder)
// 阶段2 按照pollorder顺序遍历scases,尝试读 && 写 channel。
// 按pollorder顺序遍历scases
for _, casei := range pollorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
// 是否是send操作
if casi >= nsends {
// 1. hchan是否有阻塞的发送者 ,如果有,调用chan.recv方法
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
// 2. hchan缓冲区中有数据,读缓冲区数据,返回
if c.qcount > 0 {
goto bufrecv
}
// 3. channel被关闭,此时如果是 读操作,清空channel内容。如果是写操作,panic
if c.closed != 0 {
goto rclose
}
} else {
// recv操作
// 1. channel被关闭,按lockorder解锁内存 && 抛出panic错误
if c.closed != 0 {
goto sclose
}
// hchan是否有阻塞的接收者 ,如果有,按lockorder解锁内存 && 调用chan.send方法
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
// hchan缓冲区未满,数据写入缓冲区,返回
if c.qcount < c.dataqsiz {
goto bufsend
}
}
}
// 如果有default,进入这里 ,说明所有case都没有被触发
// 直接解锁所有channel,随后return
if !block {
selunlock(scases, lockorder)
casi = -1
goto retc
}
// 阶段3 将sudog打包到所有的chan队列中,等待被唤醒
// 获得当前运行的goroutine
gp = getg()
if gp.waiting != nil {
throw("gp.waiting != nil")
}
// 根据lockorder顺序
for _, casei := range lockorder {
// 获得sudog ,将当前g与sudog绑定
sg := acquireSudog()
sg.g = gp
// 区分发送 && 接收 场景
if casi < nsends {
// 发送场景。sudog压入发送队列
c.sendq.enqueue(sg)
} else {
// 接收场景。sudog压入接收队列
c.recvq.enqueue(sg)
}
}
gp.param = nil
// 进入阻塞,等待唤醒
atomic.Store8(&gp.parkingOnChan, 1)
gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
// 被唤醒时,按lockorder顺序解锁
sellock(scases, lockorder)
// 阶段4 被唤醒后,获得顺序,并将sudog从其他chan的队列中出队
for _, casei := range lockorder {
k = &scases[casei]
if sg == sglist {
// 当前sudog是被该case唤醒的
casi = int(casei)
cas = k
caseSuccess = sglist.success
if sglist.releasetime > 0 {
caseReleaseTime = sglist.releasetime
}
} else {
// sudog从其他chan的队列中出队
c = k.c
if int(casei) < nsends {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
}
if cas == nil {
throw("selectgo: bad wakeup")
}
c = cas.c
// 按lockorder顺序解锁所有scase
selunlock(scases, lockorder)
goto retc
***
后面是一大堆goto跳出的代码块,已经在上文中分析了
}
复制代码
我们可能会好奇,为什么要多一个lockorder
- 主要目的是解决死锁
lockorder
按堆内存的顺序排序,破坏了死锁产生的循环等待
条件
高级用法
阻塞main协程
使用 select{}
可以将main协程阻塞住,但需要注意,被select{}阻塞的主协程,需要有活跃或者休眠(time.Sleep)的协程与之相关联,否则将发生panic.
猜测是与gopark
有关。
超时限制
我们可以通过监听一个 退出通道
来做到超时控制。
退出通道
可以用time.After()
也可以用context.Done()
,或者是一个程序级别的closeChannel
func doWithTimeOut(timeout time.Duration) (int, error) {
select {
case ret := <-do():
return ret, nil
case <-time.After(timeout):
return 0, errors.New("timeout")
}
}
func do() <-chan int {
outCh := make(chan int)
go func() {
// do work...
}()
return outCh
}
复制代码