Go语言使用select{}阻塞main函数介绍


Posted in Golang onApril 25, 2021

很多时候我们需要让main函数不退出,让它在后台一直执行,例如:

func main() {
    for i := 0; i < 20; i++ { //启动20个协程处理消息队列中的消息
        c := consumer.New()
        go c.Start()
    }
    select {} // 阻塞
}

可能大多数人想到阻塞的方法是用channel,当然都是可以的,不过用select{}更加简洁 :)

补充:由浅入深聊聊Golang中select的实现机制

正文

话说今天在玩select的时候发现一个问题,是这样的:

片段1:

func main(){
 var count int
 for {
  select {
  case <-time.Tick(time.Millisecond * 500):
   fmt.Println("咖啡色的羊驼")
   count++
   fmt.Println("count--->" , count)
  case <-time.Tick(time.Millisecond * 499) :
   fmt.Println(time.Now().Unix())
   count++
   fmt.Println("count--->" , count)
  }
 }
}

片段2:

func main(){
 t1 := time.Tick(time.Second)
 t2 := time.Tick(time.Second)
 var count int
 for {
  select {
  case <-t1:
   fmt.Println("咖啡色的羊驼")
   count++
   fmt.Println("count--->" , count)
  case <-t2 :
   fmt.Println(time.Now().Unix())
   count++
   fmt.Println("count--->" , count)
  }
 }
}

两个问题:

1.以上片段的输出结果是?

2.如何解释?

第一个问题好解决,跑一下就是,很明显输出结果肯定不同。

片段1:

1535673600
count---> 1
1535673600
count---> 2
1535673601
count---> 3

片段2:

咖啡色的羊驼
count---> 1
1535673600
count---> 2
咖啡色的羊驼
count---> 3
1535673601
count---> 4

第二个好理解,因为select监听了两个time的通道,所以交替出现。

那么第一个为何只有出现1个?

为了这个问题不得不把select的实现机制走一波,所以有了此文。

select机制简述

select有这么几个需要关注的机制

1.select+case是用于阻塞监听goroutine的,如果没有case,就单单一个select{},则为监听当前程序中的goroutine,此时注意,需要有真实的goroutine在跑,否则select{}会报panic

2.select底下有多个可执行的case,则随机执行一个。

3.select常配合for循环来监听channel有没有故事发生。需要注意的是在这个场景下,break只是退出当前select而不会退出for,需要用break TIP / goto的方式。

4.无缓冲的通道,则传值后立马close,则会在close之前阻塞,有缓冲的通道则即使close了也会继续让接收后面的值

5.同个通道多个goroutine进行关闭,可用recover panic的方式来判断通道关闭问题

看完以上知识点其实还是没法解释本文的核心疑惑,继续往下!

select机制详解

select的机制可以查看/src/runtime/select.go来了解。

源码片段解读:

func selectgo(sel *hselect) int {
 // ...
 // case洗牌
 pollslice := slice{unsafe.Pointer(sel.pollorder), int(sel.ncase), int(sel.ncase)}
 pollorder := *(*[]uint16)(unsafe.Pointer(&pollslice))
 for i := 1; i < int(sel.ncase); i++ {
  //....
 }
 // 给case排序
 lockslice := slice{unsafe.Pointer(sel.lockorder), int(sel.ncase), int(sel.ncase)}
 lockorder := *(*[]uint16)(unsafe.Pointer(&lockslice))
 for i := 0; i < int(sel.ncase); i++ {
  // ...
 }
 for i := int(sel.ncase) - 1; i >= 0; i-- {
  // ...
 }
 // 加锁该select中所有的channel
 sellock(scases, lockorder)
 // 进入loop
loop:
 // ... 
 // pass 1 - look for something already waiting
 // 按顺序遍历case来寻找可执行的case
 for i := 0; i < int(sel.ncase); i++ {
  //...
  switch cas.kind {
  case caseNil:
   continue
  case caseRecv:
   // ... goto xxx
  case caseSend:
   // ... goto xxx
  case caseDefault:
   dfli = casi
   dfl = cas
  }
 }
 // 没有找到可以执行的case,但有default条件,这个if里就会直接退出了。
 if dfl != nil {
  // ...
 }
 // ...
 // pass 2 - enqueue on all chans
 // chan入等待队列
 for _, casei := range lockorder {
  // ...
  switch cas.kind {
  case caseRecv:
   c.recvq.enqueue(sg)
  case caseSend:
   c.sendq.enqueue(sg)
  }
 }
 // wait for someone to wake us up
 // 等待被唤起,同时解锁channel(selparkcommit这里实现的)
 gp.param = nil
 gopark(selparkcommit, nil, "select", traceEvGoBlockSelect, 1)
 
 // 突然有故事发生,被唤醒,再次该select下全部channel加锁
 sellock(scases, lockorder)
 // pass 3 - dequeue from unsuccessful chans
 // 本轮最后一次循环操作,获取可执行case,其余全部出队列丢弃
 casi = -1
 cas = nil
 sglist = gp.waiting
 // Clear all elem before unlinking from gp.waiting.
 for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
  sg1.isSelect = false
  sg1.elem = nil
  sg1.c = nil
 }
 gp.waiting = nil
 for _, casei := range lockorder {
  // ...
  if sg == sglist {
   // sg has already been dequeued by the G that woke us up.
   casi = int(casei)
   cas = k
  } else {
   c = k.c
   if k.kind == caseSend {
    c.sendq.dequeueSudoG(sglist)
   } else {
    c.recvq.dequeueSudoG(sglist)
   }
  }
  // ...
 }
 // 没有的话,再走一次loop
 if cas == nil {
  goto loop
 }
 // ...
bufrecv:
 // can receive from buffer
bufsend:
 // ...
recv:
 // ...
rclose:
 // ...
send:
 // ...
retc:
 // ...
sclose:
 // send on closed channel
}

为了方便展示,专门搞了一张很丑的图,来说明流程:

Go语言使用select{}阻塞main函数介绍

大概就是说呢,select是分四步进行的。

本文的疑惑关键点就在于那个loop的时候,当接收到发现一个可执行的时候,本次select不会执行的那些case对应的channel给出队当前goroutine,就不管他们了,就丢了,由于time.Tick是现场在case里头创建的,而不是像片段二是处于全局栈中,所以当每次任何一个执行的时候,另一个就被抛弃了,再次selelct的时候有需要重新获取,又是新的需要重头再来。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。如有错误或未考虑完全的地方,望不吝赐教。

Golang 相关文章推荐
基于go interface{}==nil 的几种坑及原理分析
Apr 24 Golang
golang 生成对应的数据表struct定义操作
Apr 28 Golang
Golang 空map和未初始化map的注意事项说明
Apr 29 Golang
go 实现简易端口扫描的示例
May 22 Golang
Go 语言下基于Redis分布式锁的实现方式
Jun 28 Golang
Go语言空白表示符_的实例用法
Jul 04 Golang
golang实现浏览器导出excel文件功能
Mar 25 Golang
Golang获取List列表元素的四种方式
Apr 20 Golang
Golang 结构体数据集合
Apr 22 Golang
Golang解析JSON对象
Apr 30 Golang
Go gorilla securecookie库的安装使用详解
Aug 14 Golang
win10下go mod配置方式
Go语言-为什么返回值为接口类型,却返回结构体
Apr 24 #Golang
go:垃圾回收GC触发条件详解
Apr 24 #Golang
基于go interface{}==nil 的几种坑及原理分析
Apr 24 #Golang
golang interface判断为空nil的实现代码
Apr 24 #Golang
golang判断key是否在map中的代码
Apr 24 #Golang
Go语言操作数据库及其常规操作的示例代码
Apr 21 #Golang
You might like
php禁止某ip或ip地址段访问的方法
2015/02/25 PHP
ThinkPHP框架安全实现分析
2016/03/14 PHP
Yii使用migrate命令执行sql语句的方法
2016/03/15 PHP
thinkPHP3.2简单实现文件上传的方法
2016/05/16 PHP
jquery提示效果实例分析
2014/11/25 Javascript
js实现鼠标经过表格行变色的方法
2015/05/12 Javascript
jquery中val()方法是从最后一个选项往前读取的
2015/09/06 Javascript
JavaScript数据结构与算法之链表
2016/01/29 Javascript
Bootstrap中的Panel和Table全面解析
2016/06/13 Javascript
关于动态执行代码(js的Eval)实例详解
2016/08/15 Javascript
深入理解Vue.js源码之事件机制
2017/09/27 Javascript
JS实现点击下拉菜单把选择的内容同步到input输入框内的实例
2018/01/23 Javascript
原生js实现each方法实例代码详解
2019/05/27 Javascript
[01:47]2018年度DOTA2最具人气解说-完美盛典
2018/12/16 DOTA
Python3.2中Print函数用法实例详解
2015/05/19 Python
Python中exit、return、sys.exit()等使用实例和区别
2015/05/28 Python
Python数据结构与算法之图的最短路径(Dijkstra算法)完整实例
2017/12/12 Python
用Python写王者荣耀刷金币脚本
2017/12/21 Python
python日期时间转为字符串或者格式化输出的实例
2018/05/29 Python
python实现播放音频和录音功能示例代码
2018/12/30 Python
Python datetime和unix时间戳之间相互转换的讲解
2019/04/01 Python
Python中面向对象你应该知道的一下知识
2019/07/10 Python
tensorflow:指定gpu 限制使用量百分比,设置最小使用量的实现
2020/02/06 Python
Python实现Wordcloud生成词云图的示例
2020/03/30 Python
python使用建议与技巧分享(一)
2020/08/17 Python
详解HTML5中的Communication API基本使用方法
2016/01/29 HTML / CSS
加拿大最大的五金、家居装修和园艺产品商店:RONA
2017/01/27 全球购物
Desigual英国官网:在线购买原创服装
2018/03/09 全球购物
千禧酒店及度假村官方网站:Millennium Hotels and Resorts
2019/05/10 全球购物
美国Curacao百货连锁店网站:iCuracao.com
2019/07/20 全球购物
银行员工职业规划范文
2014/01/21 职场文书
厨房领班竞聘演讲稿
2014/04/23 职场文书
汽车4S店销售经理岗位职责
2015/04/02 职场文书
建筑工地资料员岗位职责
2015/04/13 职场文书
初中英语教学反思范文
2016/02/15 职场文书
高中议论文(范文2篇)
2019/08/19 职场文书