前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解go的select原理

深入理解go的select原理

作者头像
公众号-利志分享
发布2022-04-25 09:15:41
5330
发布2022-04-25 09:15:41
举报
文章被收录于专栏:利志分享

1:简介

  • go的select为golang提供了多路IO复用机制,和其他IO复用一样,用于检测是否有读写事件是否ready。 linux的系统IO模型有select,poll,epoll,go的select和linux系统select非常相似。

2:初识

  • select操作至少要有一个case语句,并且不能出现读写nil的channel,否则会报错。
  • select仅支持管道,而且是单协程操作
  • 每个case语句仅能处理一个管道,要么读要么写
  • 多个case语句的执行顺序是随机的
  • 存在default语句,select将不会阻塞,但是存在default会影响性能。

没有case的select或者case为nil的select

代码语言:javascript
复制
package main

func main() {
  select {}
}
代码语言:javascript
复制
package main

import "fmt"

func main() {
  var ch chan int
  select {
  case ch <- 1:
    fmt.Println("222")
  }
}

上面的代码均会有如下报错:

代码语言:javascript
复制
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
  E:/Server/go/src/any.com/test/t_50.go:8 +0x27

select仅能操作管道

下面是示例代码:

代码语言:javascript
复制
package main

import "fmt"

func main() {
  tmp := 3
  select {
  case tmp == 3:
    fmt.Println("222")
  }
}

执行之后报错如下:

代码语言:javascript
复制
.\t_50.go:8:11: tmp == 3 evaluated but not used
.\t_50.go:8:11: select case must be receive, send or assign recv

select每次执行只能有一个case执行,而且每个case可能执行的条件都是随机的,下面我们看个例子,当然这个例子是无法证明这个,后面我们会对源码进行分析讲解。

代码语言:javascript
复制
package main

import (
  "fmt"
  "time"
)

func main() {
  //每秒执行一个ticker
  t1 := time.NewTicker(1 * time.Second)
  //每5秒执行一个ticker
  t2 := time.NewTicker(5 * time.Second)
  //设置一个通道
  ch := make(chan int, 1)
  for {
    select {
    case <-t1.C:
      fmt.Println("t1")
      for i := 0; i <= 3; i++ {
        time.Sleep(6 * time.Second)
        fmt.Println(i)
      }
    case <-t2.C:
      fmt.Println("t2")
    case ch <- 1:
      fmt.Println("ch写入成功")
    }
  }
}

看下执行结果,都是按照每个case都可能执行到,而且顺序是不确定的。

代码语言:javascript
复制
ch写入成功
t1
0
1
2
3
t2
t1
0
1
2
3
t2
t1
0
1
2
3
t2
......

3:深入源码

  • select结构组成主要是由case语句和执行的函数组成。

我这里的go的版本是1.15.6

每个case语句的数据结构如下:

代码语言:javascript
复制
// Changes here must also be made in src/cmd/internal/gc/select.go's scasetype.
type scase struct {
  c           *hchan         // chan的结构体类型指针
  elem        unsafe.Pointer //读或者写的缓冲区地址 element
  kind        uint16  //case语句的类型,是default、传值写数据(channel <-) 还是  取值读数据(<- channel)
  pc          uintptr // 竞争检查相关用的,go run -race指令
  releasetime int64
}

在一个select中,所有的case语句一起组成scase的结构数组,然后执行select语句就是调用func selectgo(cas0 *scase, order0 *uint16, ncases int)函数。

func selectgo(cas0*scase,order0*uint16,ncasesint)(int,bool)函数参数:

  • cas0 为上文提到的case语句抽象出的结构体 scase数组的第一个元素地址
  • order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列lockorder。
  • nncases表示 scase数组的长度

selectgo返回所选scase的索引(该索引与其各自的select {recv,send,default}调用的序号位置相匹配),selectgo做了打乱case的结构体顺序,然后会随机选一个执行。如果选择的scase是接收操作(recv),则返回是否接收到值。

selectgo的调用是通过reflect_rselect函数来调用的,reflect_rselect函数的方法是rselect的实现,rselect声明是在/reflect/value.go

rselect的调用是通过/reflect/value.go里面的Select方法来调用的。

关于Select方法的调用当然就是系统来调用了。

关于select语句的底层函数调用流程我们做一下总结:

  • 系统调用func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)
  • 然后Select调用func rselect([]runtimeSelect) (chosen int, recvOK bool)
  • rselect又调用func reflect_rselect(cases []runtimeSelect) (int, bool)
  • 最后调用到func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)

下面我们来深入看下selectgo的函数,由于selectgo函数比较长,我这里大概列一下重要的点:

对所有的case进行排序,生成随机顺序,代码如下:

代码语言:javascript
复制
// generate permuted order
for i := 1; i < ncases; i++ {
  j := fastrandn(uint32(i + 1))
  pollorder[i] = pollorder[j]
  pollorder[j] = uint16(i)
}

遍历所有的case语句,如果所有的case都未就绪,则走default,如果没有default,则会阻塞。如果有就绪channel,则直接跳出顺序进行管道操作并返回。核心代码如下:

代码语言:javascript
复制
loop:
  // pass 1 - look for something already waiting
  var dfli int
  var dfl *scase
  var casi int
  var cas *scase
  var recvOK bool
  for i := 0; i < ncases; i++ {
    casi = int(pollorder[i])
    cas = &scases[casi]
    c = cas.c

    switch cas.kind {
    case caseNil:
      continue

    case caseRecv:
      sg = c.sendq.dequeue()
      if sg != nil {
        goto recv
      }
      if c.qcount > 0 {
        goto bufrecv
      }
      if c.closed != 0 {
        goto rclose
      }

    case caseSend:
      if raceenabled {
        racereadpc(c.raceaddr(), cas.pc, chansendpc)
      }
      if c.closed != 0 {
        goto sclose
      }
      sg = c.recvq.dequeue()
      if sg != nil {
        goto send
      }
      if c.qcount < c.dataqsiz {
        goto bufsend
      }

    case caseDefault:
      dfli = casi
      dfl = cas
    }
  }

  if dfl != nil {
    selunlock(scases, lockorder)
    casi = dfli
    cas = dfl
    goto retc
  }
  
retc:
  if cas.releasetime > 0 {
    blockevent(cas.releasetime-t0, 1)
  }
  return casi, recvOK

4:总结多路复用

我们看下select中的channel实现多路复用的图:

每个线程或者进程都先到图中”装置“中注册,然后阻塞,然后只有一个线程在”运输“,当注册的线程或者进程准备好数据后,”装置“会根据注册的信息得到相应的数据。从始至终kernel只会使用图中这个黄黄的线程,无需再对额外的线程或者进程进行管理,提升了效率。我这里就没有提协程了,因为最终还是系统线程去执行。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 利志分享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档