• 企业400电话
  • 网络优化推广
  • AI电话机器人
  • 呼叫中心
  • 全 部 栏 目

    网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    Go语言使用select{}阻塞main函数介绍
    POST TIME:2021-10-18 17:09

    很多时候我们需要让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
    }
    

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

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

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

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

    您可能感兴趣的文章:
    • 详解Golang并发操作中常见的死锁情形
    • Go 语言中的死锁问题解决
    • Go语言死锁与goroutine泄露问题的解决
    • golang coroutine 的等待与死锁用法
    • go select编译期的优化处理逻辑使用场景分析
    • Django实现jquery select2带搜索的下拉框
    • matplotlib之多边形选区(PolygonSelector)的使用
    • golang中的select关键字用法总结
    • Go select 死锁的一个细节
    上一篇:win10下go mod配置方式
    下一篇:Go缓冲channel和非缓冲channel的区别说明
  • 相关文章
  • 

    关于我们 | 付款方式 | 荣誉资质 | 业务提交 | 代理合作


    © 2016-2020 巨人网络通讯

    时间:9:00-21:00 (节假日不休)

    地址:江苏信息产业基地11号楼四层

    《增值电信业务经营许可证》 苏B2-20120278

    X

    截屏,微信识别二维码

    微信号:veteran88

    (点击微信号复制,添加好友)

     打开微信