深入浅出Golang中select的实现原理

目录
  • 概述
  • select实现原理
    • 执行流程
    • case数据结构
    • 执行select
    • 循环
  • 总结

概述

在go语言中,select语句就是用来监听和channel有关的IO操作,当IO操作发生时,触发相应的case操作,有了select语句,可以实现main主线程与goroutine线程之间的互动。需要的朋友可以参考以下内容,希望对大家有帮助。

select实现原理

Golang实现select时,定义了一个数据结构表示每个case语句(包含defaultdefault实际上是一种特殊的case),select执行过程可以看成一个函数,函数输入case数组,输出选中的case,然后程序流程转到选中的case块。

执行流程

在默认的情况下,select 语句会在编译阶段经过如下过程的处理:

  • 将所有的 case 转换成包含 Channel 以及类型等信息的 scase 结构体;
  • 调用运行时函数 selectgo 获取被选择的scase 结构体索引,如果当前的 scase 是一个接收数据的操作,还会返回一个指示当前case 是否是接收的布尔值;
  • 通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case

case数据结构

select控制结构中case使用了scase结构体来表示,源码包src/runtime/select.go:scase定义了表示case语句的数据结构:

type scase struct {
    c           *hchan
    elem        unsafe.Pointer
    kind        uint16
    pc          uintptr
    releasetime int64
}

scase.c:由于非defaultcase中都与channel的发送和接收数据有关,所以在scase结构体中也包含一个c字段用于存储case中使用的channel,为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel

scase.kind:表示该case的类型,分为读channel写channeldefault,三种类型分别由常量定义:

const (
    caseNil = iota
    caseRecv	//case语句中尝试读取scase.c中的数据;
    caseSend	//case语句中尝试向scase.c中写入数据;
    caseDefault //default语句
)

scase.elem:用于接收或者发送数据的变量地址,根据scase.kind不同,有不同的用途:

  • scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址;
  • scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;

执行select

在运行期间会调用selectgo()函数,这个函数主要作用是从select控制结构中的多个case中选择一个需要执行的case,随后的多个 if 条件语句就会根据 selectgo() 的返回值执行相应的语句。

运行时源码包src/runtime/select.go:selectgo()定义了select选择case的函数:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
    order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))

    scases := cas1[:ncases:ncases]
    pollorder := order1[:ncases:ncases]
    lockorder := order1[ncases:][:ncases:ncases]

    for i := range scases {
        cas := &scases[i]
        if cas.c == nil && cas.kind != caseDefault {
            *cas = scase{}
        }
    }

    for i := 1; i < ncases; i++ {
        j := fastrandn(uint32(i + 1))
        pollorder[i] = pollorder[j]
        pollorder[j] = uint16(i)
    }

    // sort the cases by Hchan address to get the locking order.
    // ...

    sellock(scases, lockorder)

    // ...
}

selectgo 函数首先会进行执行一些初始化操作,也就是决定处理 case 的两个顺序,其中一个是 pollOrder 另一个是 lockOrder

函数参数:

  • cas0:为scase数组的首地址,selectgo()就是从这些scase中找出一个返回。
  • order0:为一个两倍cas0数组长度的buffer,保存scase随机序列pollorderscasechannel地址序列lockorder
  • pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。
  • lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。
  • ncases:表示scase数组的长度

函数返回值:

  • int: 选中case的编号,这个case编号跟代码一致
  • bool: 是否成功从channle中读取了数据,如果选中的case是从channel中读数据,则该返回值表示是否读取成功。

循环

当 select 语句确定了轮询和锁定的顺序并锁定了所有的 Channel 之后就会开始进入 select的主循环,查找或者等待 Channel 准备就绪,循环中会遍历所有的 case 并找到需要被唤起的sudog 结构体。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    // ...
    gp = getg()
    nextp = &gp.waiting
    for _, casei := range lockorder {
        casi = int(casei)
        cas = &scases[casi]
        if cas.kind == caseNil {
            continue
        }
        c = cas.c
        sg := acquireSudog()
        sg.g = gp
        sg.isSelect = true
        sg.elem = cas.elem
        sg.c = c
        *nextp = sg
        nextp = &sg.waitlink

        switch cas.kind {
        case caseRecv:
            c.recvq.enqueue(sg)

        case caseSend:
            c.sendq.enqueue(sg)
        }
    }

    gp.param = nil
    gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)

    // ...
}

在这段循环的代码中,我们会分四种不同的情况处理 select 中的多个 case

caseNil — 当前 case 不包含任何的 Channel,就直接会被跳过;

caseRecv — 当前 case 会从 Channel 中接收数据;

  • 如果当前 Channel 的 sendq 上有等待的 Goroutine 就会直接跳到 recv 标签所在的代码段,从 Goroutine 中获取最新发送的数据;
  • 如果当前 Channel 的缓冲区不为空就会跳到 bufrecv 标签处从缓冲区中获取数据;
  • 如果当前 Channel 已经被关闭就会跳到rclose 做一些清除的收尾工作;

caseSend — 当前 case 会向 Channel 发送数据;

  • 如果当前 Channel 已经被关闭就会直接跳到 rclose 代码段;
  • 如果当前 Channel 的 recvq 上有等待的 Goroutine 就会跳到 send 代码段向 Channel 直接发送数据;

caseDefault — 表示默认情况,如果循环执行到了这种情况就表示前面的所有case 都没有被执行,所以这里会直接解锁所有的 Channel 并退出 selectgo 函数,这时也就意味着当前 select 结构中的其他收发语句都是非阻塞的。

总结

通过以上内容我们简单的了解了select结构的执行过程与实现原理,首先在编译期间,Go 语言会对 select 语句进行优化, 对于空的select语句会直接转换成block函数的调用,直接挂起当前Goroutine,如果select语句中只包含一个case,就会被转换成if ch == nil {block}; n; 表达式。然后执行case结构体中内容。

在运行时会执行selectgo函数,随机生成一个遍历的轮询顺序pollOrder并根据Channel地址生成一个用于遍历的锁定顺序lockOrder;然后根据pollOrder遍历所有的case查看是否有可以处理的Channel消息,如果有消息就直接获取case对应的索引并返回。如果没有消息就会创建sudog结构体,将当前 Goroutine 加入到所有相关 Channel sendq  recvq 队列中并调用 gopark 触发调度器的调度;

注意: 并不是所有的select控制结构都会走到selectgo,很多情况都会被直接优化调。

以上就是深入浅出Golang中select的实现原理的详细内容,更多关于Golang select原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • golang中select语句的简单实例

    目录 前言 1.先举个简单例子 2. 避免造成死锁 3. select 随机性 4. select 的超时 5. 读取/写入都可以 6. 总结一下 前言 在golang语言中,select语句 就是用来监听和channel有关的IO操作,当IO操作发生时,触发相应的case动作.有了 select语句,可以实现 main主线程 与 goroutine线程 之间的互动. select { case <-ch1 : // 检测有没有数据可读 // 一旦成功读取到数据,则进行该case处理语句 cas

  • go select的用法

    目录 1. select语句只能用于信道的读写操作 2. select中的case语句是随机执行的 3. 对于case条件语句中,如果存在通道值为nil的读写操作,则该分支将被忽略 4. 超时用法 5. 空select{} 6. for中的select 引起的CPU过高的问题 补充:7. 使用 select 切换协程 golang中的select语句格式如下 select {     case <-ch1:         // 如果从 ch1 信道成功接收数据,则执行该分支代码     cas

  • 简介Go语言中的select语句的用法

    在Go编程语言中的select语句的语法如下: 复制代码 代码如下: select {     case communication clause  :        statement(s);          case communication clause  :        statement(s);     /* you can have any number of case statements */     default : /* Optional */        stat

  • Go select使用与底层原理讲解

    目录 1. select的使用 2. 底层原理 3. 数据结构 4. 几种常见 case case 1 case2 case3 case4 1. select的使用 select 是 Go 提供的 IO 多路复用机制,可以用多个 case 同时监听多个 channl 的读写状态: case: 可以监听 channl 的读写信号 default:声明默认操作,有该字段的 select 不会阻塞 select { case chan <-: // TODO case <- chan: // TOD

  • Go语言中Select语句用法实例

    本文实例讲述了Go语言中Select语句用法.分享给大家供大家参考.具体分析如下: select 语句使得一个 goroutine 在多个通讯操作上等待. select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支.当多个都准备好的时候,会随机选择一个. 复制代码 代码如下: package main import "fmt" func fibonacci(c, quit chan int) {         x, y := 1, 1         for {

  • golang中的select关键字用法总结

    1.官方解释 一个select语句用来选择哪个case中的发送或接收操作可以被立即执行.它类似于switch语句,但是它的case涉及到channel有关的I/O操作.即select就是用来监听和channel有关的IO操作,当 IO 操作发生时,触发相应的动作. 2.要点 如果有一个或多个IO操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进

  • Go语言select语句用法示例

    目录 用法 使用场景 实现收发功能 注意事项 用法 多个通道 Channel 中信息的发送和接受处理的专用的语句—select 语句.select 语句会阻塞,直到其中的一个发送/接收操作准备好.select 语句和 switch 语句有点相似,但 select 语句在被执行时会选择执行其中的一个分支,且选择分支的方法完全是不相同的. ch1 = make(chan string) ch2 = make(chan string) ch1 <- "server1" ch2 <

  • 深入浅出Golang中select的实现原理

    目录 概述 select实现原理 执行流程 case数据结构 执行select 循环 总结 概述 在go语言中,select语句就是用来监听和channel有关的IO操作,当IO操作发生时,触发相应的case操作,有了select语句,可以实现main主线程与goroutine线程之间的互动.需要的朋友可以参考以下内容,希望对大家有帮助. select实现原理 Golang实现select时,定义了一个数据结构表示每个case语句(包含default,default实际上是一种特殊的case),

  • 一文带你了解Golang中select的实现原理

    目录 概述 结构 现象 非阻塞的收发 随机执行 编译 直接阻塞 独立情况 非阻塞操作 通用情况 运行时 初始化 循环 总结 概述 select是go提供的一种跟并发相关的语法,非常有用.本文将介绍 Go 语言中的 select 的实现原理,包括 select 的结构和常见问题.编译期间的多种优化以及运行时的执行过程. select 是一种与 switch 非常相似的控制结构,与 switch 不同的是,select 中虽然也有多个 case,但是这些 case 中的表达式都必须与 Channel

  • 详解Golang中select的使用与源码分析

    目录 背景 select 流程 背景 golang 中主推 channel 通信.单个 channel 的通信可以通过一个goroutine往 channel 发数据,另外一个从channel取数据进行.这是阻塞的,因为要想顺利执行完这个步骤,需要 channel 准备好才行,准备好的条件如下: 1.发送 缓存有空间(如果是有缓存的 channel) 有等待接收的 goroutine 2.接收 缓存有数据(如果是有缓存的 channel) 有等待发送的 goroutine 对channel实际使

  • Golang中channel的原理解读(推荐)

    数据结构 channel的数据结构在$GOROOT/src/runtime/chan.go文件下: type hchan struct { qcount uint // 当前队列中剩余元素个数 dataqsiz uint // 环形队列长度,即可以存放的元素个数 buf unsafe.Pointer // 环形队列指针 elemsize uint16 // 每个元素的大小 closed uint32 // 标记是否关闭 elemtype *_type // 元素类型 sendx uint //

  • 一文详解Golang中net/http包的实现原理

    目录 前言 http包执行流程 http包源码分析 端口监听 请求解析 路由分配 响应处理 前言 Go语言自带的net/http包提供了HTTP客户端和服务端的实现,实现一个简单的http服务非常容易,其自带了一些列结构和方法来帮助开发者简化HTTP服务开发的相关流程,因此我们不需要依赖任何第三方组件就能构建并启动一个高并发的HTTP服务器,net/http包在编写web应用中有很重要的作用,这篇文章会学习如何用 net/http 自己编写实现一个 HTTP Server 并探究其实现原理,具体

  • 详解Golang中Context的原理和使用技巧

    目录 Context 背景 和 适用场景 Context 的背景 Context 的功能和目的 Context 的基本使用 Context 的同步控制设计 Context 的定义和实现 Context interface 接口定义 parent Context 的具体实现 Context 的继承和各种 With 系列函数 Context 的常用方法实例 1. 调用 Context Done方法取消 2. 通过 context.WithValue 来传值 3. 超时取消 context.WithT

  • Golang中switch语句和select语句的用法教程

    本文主要给大家介绍了关于Golang中switch和select用法的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 一.switch语句 switch语句提供了一个多分支条件执行的方法.每一个case可以携带一个表达式或一个类型说明符.前者又可被简称为case表达式.因此,Go语言的switch语句又分为表达式switch语句和类型switch语句. 1.表达式switch语句 var name string ... switch name { case "Golang"

  • 详解golang中bufio包的实现原理

    最近用golang写了一个处理文件的脚本,由于其中涉及到了文件读写,开始使用golang中的 io 包,后来发现golang 中提供了一个bufio的包,使用这个包可以大幅提高文件读写的效率,于是在网上搜索同样的文件读写为什么bufio 要比io的读写更快速呢?根据网上的资料和阅读源码,以下来详细解释下bufio的高效如何实现的. bufio 包介绍  bufio包实现了有缓冲的I/O.它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文

随机推荐