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

目录
  • 背景
  • select 流程

背景

golang 中主推 channel 通信。单个 channel 的通信可以通过一个goroutinechannel 发数据,另外一个从channel取数据进行。这是阻塞的,因为要想顺利执行完这个步骤,需要 channel 准备好才行,准备好的条件如下:

1.发送

  • 缓存有空间(如果是有缓存的 channel)
  • 有等待接收的 goroutine

2.接收

  • 缓存有数据(如果是有缓存的 channel)
  • 有等待发送的 goroutine

channel实际使用中还有如下两个需求,这个时候就需要select了。

  • 同时监听多个channel
  • 在没有channel准备好的时候,也可以往下执行。

select 流程

1.空select。作用是阻塞当前goroutine。不要用for{}来阻塞goroutine,因为会占用cpu。而select{}不会,因为当前goroutine不会再被调度。

 if len(cases) == 0 {
         block()
 }

2.配置好poll的顺序。由于是同时监听多个channel的发送或者接收,所以需要按照一定的顺序查看哪个channel准备好了。如果每次采用select中的顺序查看channel是否准备好了,那么只要在前面的channel准备好的足够快,那么会造成后面的channel即使准备好了,也永远不会被执行。打乱顺序的逻辑如下,采用了洗牌算法\color{red}{洗牌算法}洗牌算法,注意此过程中会过滤掉channel为nil的case。\color{red}{注意此过程中会过滤掉 channel 为 nil 的 case。}注意此过程中会过滤掉channel为nil的case。

 // generate permuted order
 norder := 0
 for i := range scases {
         cas := &scases[i]

         // Omit cases without channels from the poll and lock orders.
         if cas.c == nil {
                 cas.elem = nil // allow GC
                 continue
         }

         j := fastrandn(uint32(norder + 1))
         pollorder[norder] = pollorder[j]
         pollorder[j] = uint16(i)
         norder++
 }

3.配置好lock的顺序。由于可能会修改channel中的数据,所以在打算往channel中发送数据或者从channel接收数据的时候,需要锁住 channel。而一个channel可能被多个select监听,如果两个select对两个channel A和B,分别按照顺序A, B和B,A上锁,是可能会造成死锁的,导致两个select都执行不下去。

所以select中锁住channel的顺序至关重要,解决方案是按照channel的地址的顺序锁住channel。因为在两个selectchannel有交集的时候,都是按照交集中channel的地址顺序锁channel

实际排序代码如下,采用堆排序算法\color{red}{堆排序算法}堆排序算法按照channel的地址从小到大对channel进行排序。

 // sort the cases by Hchan address to get the locking order.
 // simple heap sort, to guarantee n log n time and constant stack footprint.
 for i := range lockorder {
         j := i
         // Start with the pollorder to permute cases on the same channel.
         c := scases[pollorder[i]].c
         for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
                 k := (j - 1) / 2
                 lockorder[j] = lockorder[k]
                 j = k
         }
         lockorder[j] = pollorder[i]
 }
 for i := len(lockorder) - 1; i >= 0; i-- {
         o := lockorder[i]
         c := scases[o].c
         lockorder[i] = lockorder[0]
         j := 0
         for {
                 k := j*2 + 1
                 if k >= i {
                         break
                 }
                 if k+1 < i && scases[lockorder[k]].c.sortkey() < scases[lockorder[k+1]].c.sortkey() {
                         k++
                 }
                 if c.sortkey() < scases[lockorder[k]].c.sortkey() {
                         lockorder[j] = lockorder[k]
                         j = k
                         continue
                 }
                 break
         }
         lockorder[j] = o
 }

4.锁住select中的所有channel。要查看channel中的数据了。

 // lock all the channels involved in the select
 sellock(scases, lockorder)

5.第一轮查看是否已有准备好的channel。如果有直接发送数据到channel或者从channel接收数据。注意selectchannel切片中,前面部分是从channel接收数据的case,后半部分是往channel发送数据的case。

按照pollorder顺序查看是否有channel准备好了。

 for _, casei := range pollorder {
         casi = int(casei)
         cas = &scases[casi]
         c = cas.c
         if casi >= nsends {
                 sg = c.sendq.dequeue()
                 if sg != nil {
                         goto recv
                 }
                 if c.qcount > 0 {
                         goto bufrecv
                 }
                 if c.closed != 0 {
                         goto rclose
                 }
         } else {
                 if raceenabled {
                         racereadpc(c.raceaddr(), casePC(casi), chansendpc)
                 }
                 if c.closed != 0 {
                         goto sclose
                 }
                 sg = c.recvq.dequeue()
                 if sg != nil {
                         goto send
                 }
                 if c.qcount < c.dataqsiz {
                         goto bufsend
                 }
         }
 }

6.直接执行default分支

 if !block {
         selunlock(scases, lockorder)
         casi = -1
         goto retc
 }

7.第二轮遍历channel。创建sudog把当前goroutine放到每个channel的等待列表中去,等待channel准备好时被唤醒。

 // pass 2 - enqueue on all chans
 gp = getg()
 if gp.waiting != nil {
         throw("gp.waiting != nil")
 }
 nextp = &gp.waiting
 for _, casei := range lockorder {
         casi = int(casei)
         cas = &scases[casi]
         c = cas.c
         sg := acquireSudog()
         sg.g = gp
         sg.isSelect = true
         // No stack splits between assigning elem and enqueuing
         // sg on gp.waiting where copystack can find it.
         sg.elem = cas.elem
         sg.releasetime = 0
         if t0 != 0 {
                 sg.releasetime = -1
         }
         sg.c = c
         // Construct waiting list in lock order.
         *nextp = sg
         nextp = &sg.waitlink

         if casi < nsends {
                 c.sendq.enqueue(sg)
         } else {
                 c.recvq.enqueue(sg)
         }
 }

8.等待被唤醒。其中gopark的时候会释放对所有channel占用的锁。

 // wait for someone to wake us up
 gp.param = nil
 // Signal to anyone trying to shrink our stack that we're about
 // to park on a channel. The window between when this G's status
 // changes and when we set gp.activeStackChans is not safe for
 // stack shrinking.
 atomic.Store8(&gp.parkingOnChan, 1)
 gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
 gp.activeStackChans = false

9.被唤醒

  • 锁住所有channel
  • 清理当前goroutine的等待sudog
  • 找到是被哪个channel唤醒的,并清理每个channel上当前的goroutine对应的sudog
 sellock(scases, lockorder)

 gp.selectDone = 0
 sg = (*sudog)(gp.param)
 gp.param = nil

 // pass 3 - dequeue from unsuccessful chans
 // otherwise they stack up on quiet channels
 // record the successful case, if any.
 // We singly-linked up the SudoGs in lock order.
 casi = -1
 cas = nil
 caseSuccess = false
 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 {
         k = &scases[casei]
         if sg == sglist {
                 // sg has already been dequeued by the G that woke us up.
                 casi = int(casei)
                 cas = k
                 caseSuccess = sglist.success
                 if sglist.releasetime > 0 {
                         caseReleaseTime = sglist.releasetime
                 }
         } else {
                 c = k.c
                 if int(casei) < nsends {
                         c.sendq.dequeueSudoG(sglist)
                 } else {
                         c.recvq.dequeueSudoG(sglist)
                 }
         }
         sgnext = sglist.waitlink
         sglist.waitlink = nil
         releaseSudog(sglist)
         sglist = sgnext
 }

到此这篇关于详解Golang中select的使用与源码分析的文章就介绍到这了,更多相关Golang select内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 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的用法

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

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

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

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

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

  • 详解SpringBoot启动代码和自动装配源码分析

    目录 一.SpringBoot启动代码主线分析 二.SpringBoot自动装配原理分析 1.自动装配的前置知识@Import 2.@SpringApplication注解分析 2.1@SpringBootConfiguration 2.2@EnableAutoConfiguration ​随着互联网的快速发展,各种组件层出不穷,需要框架集成的组件越来越多.每一种组件与Spring容器整合需要实现相关代码.SpringMVC框架配置由于太过于繁琐和依赖XML文件:为了方便快速集成第三方组件和减少

  • 一文详解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中的各种时间操作

    需求 时间格式的转换比较麻烦,自己写了个工具,可以通过工具中的这些方法相互调用转成自己想要的格式,代码如下,后续有新的函数再添加 实现代码 package utils import "time" const ( TIMEFORMAT = "20060102150405" NORMALTIMEFORMAT = "2006-01-02 15:04:05" ) // 当前时间 func GetTime() time.Time{ return time.

  • 详解Golang中Channel的用法

    如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制.一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息. 1 创建channel 每个channel都有一个特殊的类型,也就是channels可发送数据的类型.一个可以发送int类型数据 的channel一般写为chan int.使用内置的make函数,如果第二个参数大于0,则表示创建一个带缓存的channel. ch := make(chan in

  • 详解golang中的method

    什么是method(方法)?method是函数的另外一种形态,隶属于某个类型的方法. method的语法: func (r Receiver) funcName (parameters) (result) receiver可以看作是method的第一个参数,method并且支持继承和重写. Go中虽没有class,但依旧有method 通过显示说明receiver来实现与某个类型的结合 只能为同一个包中的类型定义方法 receiver可以是类型的值或者指针 不存在方法重载 可以使用值或指针来调用

  • 一文详解Golang中的切片数据类型

    目录 含义 定义 三个要素 切片与数组的区别 示例代码 切片内存分布 切片定义分类 数组生成切片 示例代码 切片索引 直接声明切片 定义语法 代码示例 使用make定义切片 常用操作 长度计算 容量计算 判断是否为空 切片追加 语法格式 尾部追加 开始位置追加 中间位置追加 复制 引用和复制 切片的删除 删除开头 删除中间 删除结尾 指定位置 排序 迭代器 含义 切片是一个种特殊的数组.是对数组的一个连续片段的引用,所以切片是一个引用类型.切片可以是数组中的一部分,也可以是由起始和终止索引标识的

  • 详解Golang中字符串的使用

    目录 1.字符串编码 2.字符串遍历 3.字符串中的字符数 4.字符串trim 5.字符串连接 6.字节切片转字符串 1.字符串编码 在go中rune是一个unicode编码点. 我们都知道UTF-8将字符编码为1-4个字节,比如我们常用的汉字,UTF-8编码为3个字节.所以rune也是int32的别名. type rune = int32 当我们打印一个英文字符hello的时候,我们可以得到s的长度为5,因为英文字母代表1个字节: package main import "fmt"

  • 一文详解Golang中consul的基本使用

    目录 consul consul的安装和部署 docker安装 consul镜像的启动 启动一个tcp_health_check的服务注册 http版 服务发现 consul consul是一个开源服务注册和服务发现的中心,可以用于微服务的注册和服务之间的调用的发现,帮助上游服务找到下游服务的具体ip:port或者是domain,也可以使用dns的方式让consul帮你去做转发,具体介绍请看consul的官网,consul区分server-agent和client-agent,client-ag

随机推荐