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 相关文章推荐
Golang二维切片初始化的实现
Apr 08 Golang
基于Go Int转string几种方式性能测试
Apr 28 Golang
golang协程池模拟实现群发邮件功能
May 02 Golang
go类型转换及与C的类型转换方式
May 05 Golang
解决Goland 同一个package中函数互相调用的问题
May 06 Golang
浅谈Golang 切片(slice)扩容机制的原理
Jun 09 Golang
再次探讨go实现无限 buffer 的 channel方法
Jun 13 Golang
基于Go语言构建RESTful API服务
Jul 25 Golang
修改并编译golang源码的操作步骤
Jul 25 Golang
golang 语言中错误处理机制
Aug 30 Golang
Golang中channel的原理解读(推荐)
Oct 16 Golang
golang为什么要统一错误处理
Apr 03 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 文章中的远程图片采集到本地的代码
2009/07/30 PHP
PHP 工厂模式使用方法
2010/05/18 PHP
php中把美国时间转为北京时间的自定义函数分享
2014/07/28 PHP
mod_php、FastCGI、PHP-FPM等PHP运行方式对比
2015/07/02 PHP
PHP中explode函数和split函数的区别小结
2016/08/24 PHP
PHP实现文件下载【实例分享】
2017/04/28 PHP
PHP基于socket实现的简单客户端和服务端通讯功能示例
2017/07/10 PHP
php注册系统和使用Xajax即时验证用户名是否被占用
2017/08/31 PHP
PHP 的Opcache加速的使用方法
2017/12/29 PHP
document.open() 与 document.write()的区别
2007/08/13 Javascript
批量实现面向对象的实例代码
2013/07/01 Javascript
js形成页面的一种遮罩效果实例代码
2014/01/04 Javascript
jQuery动态创建html元素的常用方法汇总
2014/09/05 Javascript
JavaScript实现动态创建CSS样式规则方案
2014/09/06 Javascript
JavaScript中解析JSON数据的三种方法
2015/07/03 Javascript
js纯数字逐一停止显示效果的实现代码
2016/03/16 Javascript
基于JS实现EOS隐藏错误提示层代码
2016/04/25 Javascript
Javascript this 函数深入详解
2016/12/13 Javascript
JS检测数组类型的方法小结
2017/03/14 Javascript
JavaScript实现音乐自动切换和轮播
2017/11/05 Javascript
Next.js实现react服务器端渲染的方法示例
2019/01/06 Javascript
微信小程序地图导航功能实现完整源代码附效果图(推荐)
2019/04/28 Javascript
微信小程序登录数据解密及状态维持实例详解
2019/05/06 Javascript
Vue 修改网站图标的方法
2020/12/31 Vue.js
通过python下载FTP上的文件夹的实现代码
2013/02/10 Python
将图片文件嵌入到wxpython代码中的实现方法
2014/08/11 Python
详解Python中的Numpy、SciPy、MatPlotLib安装与配置
2017/11/17 Python
python 实现对数据集的归一化的方法(0-1之间)
2018/07/17 Python
python自制包并用pip免提交到pypi仅安装到本机【推荐】
2019/06/03 Python
python+mysql实现个人论文管理系统
2019/10/25 Python
Beauty Expert美国/加拿大:购买奢侈美容产品
2018/12/05 全球购物
关于热爱祖国的演讲稿
2014/05/04 职场文书
村级换届选举方案
2014/05/10 职场文书
中秋联欢会主持词
2015/07/04 职场文书
高三物理教学反思
2016/02/20 职场文书
mybatis 解决从列名到属性名的自动映射失败问题
2021/06/30 Java/Android