脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - 深入浅出Golang中select的实现原理

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

2022-11-09 11:29yi个俗人 Golang

在go语言中,select语句就是用来监听和channel有关的IO操作,当IO操作发生时,触发相应的case操作,有了select语句,可以实现main主线程与goroutine线程之间的互动。本文就来详细讲讲select的实现原理,需要的可以参考一下

概述

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

select实现原理

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

执行流程

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

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

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

case数据结构

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

?
1
2
3
4
5
6
7
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,三种类型分别由常量定义:

?
1
2
3
4
5
6
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的函数:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 结构体。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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原理的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7132355757944619039

延伸 · 阅读

精彩推荐
  • Golang详解Golang语言中的interface

    详解Golang语言中的interface

    这篇文章主要介绍了Golang语言中的interface的相关资料,帮助大家更好的理解和使用golang,感兴趣的朋友可以了解下...

    雨燕3872021-03-24
  • Golang对Golang中的runtime.Caller使用说明

    对Golang中的runtime.Caller使用说明

    这篇文章主要介绍了对Golang中的runtime.Caller使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    思维的深度4462021-03-19
  • Golanggolang 实现对Map进行键值自定义排序

    golang 实现对Map进行键值自定义排序

    这篇文章主要介绍了golang 实现对Map进行键值自定义排序,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    2299973711182021-06-05
  • Golang解决golang json解析出现值为空的问题

    解决golang json解析出现值为空的问题

    这篇文章主要介绍了解决golang json解析出现值为空的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    一只APE的进化史13092021-03-22
  • GolangGolang 语言怎么避免空指针引发的 Panic

    Golang 语言怎么避免空指针引发的 Panic

    本文我们介绍一些 Golang 语言开发需要避免空指针引发 panic 的场景,虽然都比较简单,但是新手很容易踩“坑”。欢迎读者朋友们在评论区与大家分享更多...

    Golang语言开发栈11282022-01-05
  • GolangGO的range具体使用

    GO的range具体使用

    GO语言的for…range 能做什么呢?golang的for…range是go 身的语法,可以用来遍历数据结构,本文就详细的来介绍一下具体使用,感兴趣的可以了解一下...

    小魔童哪吒3932021-11-25
  • GolangGo语言defer语句的三种机制整理

    Go语言defer语句的三种机制整理

    在本篇文章里小编给大家分享的是一篇关于Go语言defer语句的三种机制整理,需要的朋友们学习下吧。 ...

    面向人生编程4712020-06-06
  • GolangGo语言中字符串的查找方法小结

    Go语言中字符串的查找方法小结

    这篇文章主要介绍了Go语言中字符串的查找方法小结,示例的main函数都是导入strings包然后使用其中的方法,需要的朋友可以参考下 ...

    脚本之家14422020-04-27