【Go底层】select原理

目录

  • 1、背景
  • 2、go版本
  • 3、 selectgo函数解释
    • 【1】函数参数解释
    • 【2】函数具体解释
      • 第一步:遍历pollorder,选出准备好的case
      • 第二步:将当前goroutine放到所有case通道中对应的收发队列上
      • 第三步:唤醒groutine
  • 4、总结

1、背景

select多路复用在go的异步和并发控制场景中非常好用,对于无case和只有单个case的情况,编译器在编译的时候就会对其做优化,无case就相当于调用了一个阻塞函数,单个case就相当于对一个通道进行读写操作,如果单个case中有default分支时,就相当于是一个if else逻辑,对于多个case的情况,是在运行时调用selectgo函数决定的,接下来我们就来研究一下selectgo函数。

2、go版本

$ go version
go version go1.21.4 windows/386

3、 selectgo函数解释

【1】函数参数解释

selectgo函数位于:src/runtime/select.go中,定义如下:

//cas0:case数组地址,按照往通道写数据在前,从通道读数据在后的排列顺序(编译时编译器优化行为操作的)
//nsends:往通道写数据的case数量
//nrecvs:从通道读数据的case数量
//block:是否阻塞
//返回值分别代表选中规定case位置和是否成功从通道接收数据,如果选中的是default,第一个返回值就返回-1
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool)

select中每一个case都对应一个scase结构,定义如下:

type scase struct {c    *hchan         //case对应的读或写通道elem unsafe.Pointer //指向要写入元素或存放读取元素的地址
}

【2】函数具体解释

selectgo函数中会遍历所有的case,为确保遍历case的随机性和安全性,有两个关键的顺序:pollorder和lockorder,不用关心其具体实现,明白其的作用就行。

pollorder:随机的case顺序,确保公平的处理每一个case。
lockorder:加锁的case顺序,确保并发安全。

计算出pollorder和lockorder顺序之后,会根据这2个顺序进行遍历分为了3步。

第一步:遍历pollorder,选出准备好的case

第一部分的代码如下:

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {...var casi int   //准备好的case位置var cas *scase //case对象var caseSuccess boolvar caseReleaseTime int64 = -1var recvOK bool //如果是从通道读取数据,是否读取成功for _, casei := range pollorder { //遍历随机顺序的casecasi = int(casei)   //case的位置cas = &scases[casi] //case对象c = cas.c //case通道if casi >= nsends { //前面讲过,写通道在前,读通道在后,所以这里是读通道casesg = c.sendq.dequeue() //取出往读通道写数据的协程队列中的第一个协程if sg != nil { //如果存在往通道写数据的协程goto recv  //从往通道写数据的协程中读取数据并返回case位置和读取结果}if c.qcount > 0 { //如果缓冲区还有数据goto bufrecv  //从缓冲区读取数据并返回case位置和读取结果}if c.closed != 0 { //如果通道已关闭goto rclose    //释放相关资源}} else { //写通道的caseif raceenabled {racereadpc(c.raceaddr(), casePC(casi), chansendpc)}if c.closed != 0 { //如果通道已经关闭goto sclose    //直接panic}sg = c.recvq.dequeue() //从正在往通道读数据的协程队列中取得第一个if sg != nil { //如果往通道读数据的协程存在goto send  //发送数据到读通道的协程}if c.qcount < c.dataqsiz { //缓冲区还有位置goto bufsend}}}if !block { //如果不阻塞,也就是带default分支selunlock(scases, lockorder)casi = -1 //case位置为-1goto retc //直接返回,不用进入下一步}...
}

bufrecv标签:

	bufrecv:recvOK = true  //返回读数据成功qp = chanbuf(c, c.recvx) //缓冲区中要读取数据的地址if cas.elem != nil {typedmemmove(c.elemtype, cas.elem, qp) //将读取的缓冲区数据拷贝到case中的elem位置}typedmemclr(c.elemtype, qp) //清理缓冲区被读的数据c.recvx++ //读取缓冲区的位置+1if c.recvx == c.dataqsiz { //下一个要读取缓冲区的位置如果等于缓冲区大小就将下次要读取的缓冲区位置置为0c.recvx = 0}c.qcount-- //缓冲区中元素个数-1selunlock(scases, lockorder)goto retc

bufsend标签:

	bufsend:typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem) //将case中要写入的元素写到缓冲区c.sendx++ //写入缓冲区的位置+1if c.sendx == c.dataqsiz { //如果下次要写入缓冲区的位置等于缓冲区的大小就将缓冲区写入位置置为开头c.sendx = 0}c.qcount++ //缓冲区元素数量+1selunlock(scases, lockorder)goto retc	

recv标签:

recv:recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) //从写通道的协程读取数据if debugSelect {print("syncrecv: cas0=", cas0, " c=", c, "\n")}recvOK = true //返回成功读取goto retc

rclose标签:

rclose:selunlock(scases, lockorder)recvOK = false //从通道中读取数据失败if cas.elem != nil {typedmemclr(c.elemtype, cas.elem) //释放case中元素的空间}if raceenabled {raceacquire(c.raceaddr())}goto retc

send标签:

send:send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) //发送数据到往通道读数据的协程if debugSelect {print("syncsend: cas0=", cas0, " c=", c, "\n")}goto retc

retc标签:

retc:if caseReleaseTime > 0 {blockevent(caseReleaseTime-t0, 1)}return casi, recvOK  //返回case位置和是否从通道成功读取数据

sclose标签:

sclose:selunlock(scases, lockorder)panic(plainError("send on closed channel"))

上面就是selectgo函数第一部分的逻辑,第一部分就是遍历一个随机的case顺序,如果有符合条件的case就返回case的位置并且返回读数据的结果,如果没有case符合条件但是有default分支就返回-1,如果没default分支就进入下一步。

第二步:将当前goroutine放到所有case通道中对应的收发队列上

第二部分的代码如下:

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {...gp = getg() //获取当前协程if gp.waiting != nil {throw("gp.waiting != nil")}nextp = &gp.waitingfor _, casei := range lockorder { //按照对case加锁的顺序遍历casecasi = int(casei)   //case的位置cas = &scases[casi] //case对象c = cas.c  //case对象中的通道sg := acquireSudog() //初始化一个协程等待结构sg.g = gp //协程等待结构绑定协程sg.isSelect = true //表示该协程等待结构与select操作相关sg.elem = cas.elem sg.releasetime = 0if t0 != 0 {sg.releasetime = -1}sg.c = c*nextp = sgnextp = &sg.waitlinkif casi < nsends { //如果case上是往通道写数据,就将绑定当前协程的等待对象插入当前case通道的发送队列中c.sendq.enqueue(sg) } else { //如果case上是往通道读数据,就将绑定当前协程的等待对象插入当前case通道的接收队列中c.recvq.enqueue(sg)}}...
}

第二部分就是将当前协程放到每个case中的通道对应的收发队列中去。

第三步:唤醒groutine

第三部分代码如下:

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {...sg = (*sudog)(gp.param) //被唤醒的协程等待结构gp.param = nilcasi = -1  //case位置cas = nil  //case对象caseSuccess = falsesglist = gp.waiting //lockorder顺序的协程等待结构队列,这里是队列中的第一个协程等待结构for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink { //清空协程等待结构队列中元素便于进行垃圾回收sg1.isSelect = falsesg1.elem = nilsg1.c = nil}gp.waiting = nilfor _, casei := range lockorder { //根据对case的加锁顺序进行遍历k = &scases[casei] //当前caseif sg == sglist {  //唤醒的协程等待结构是当前case的casi = int(casei) //唤醒的case位置cas = k //唤醒的case对象caseSuccess = sglist.success //往通道读取或写数据结果if sglist.releasetime > 0 {caseReleaseTime = sglist.releasetime}} else { //唤醒的协程等待结构不是当前case的c = k.cif int(casei) < nsends { //case为发送通道,就是释放当前case通道里sendq队列的协程等待结构对象c.sendq.dequeueSudoG(sglist)} else {  //case为读取通道,就是释放当前case通道里recvq队列的协程等待结构对象c.recvq.dequeueSudoG(sglist)}}sgnext = sglist.waitlink //下一个协程等待结构sglist.waitlink = nilreleaseSudog(sglist) //释放上一个协程等待结构sglist = sgnext}...
}

第三部分就是某一个case上的协程等待结构被唤醒时,会先执行通道上对应的收发操作, 然后去将所有case上的协程等待结构释放掉。

4、总结

select虽然使用起来简单,但其实现逻辑还是比较复杂的,通过熟悉其实现,我们能理解对多个通道进行操作时候,可以为每一个通道创建一个协程去操作,这无疑增加了GC开销,但是使用select采用了多路复用的思想,将一个协程绑定在多个协程等待对象上,而且对case使用了随机顺序,确保每一个case都能公平的被执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/61802.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spark和MapReduce场景应用和区别

文章目录 Spark和MapReduce场景应用和区别一、引言二、MapReduce和Spark的应用场景1. MapReduce的应用场景2. Spark的应用场景 三、MapReduce和Spark的区别1. 内存使用和性能2. 编程模型和易用性3. 实时计算支持 四、使用示例1. MapReduce代码示例2. Spark代码示例 五、总结 Sp…

Python办公——openpyxl处理Excel每个sheet每行 修改为软雅黑9号剧中+边框线

目录 专栏导读背景1、库的介绍①&#xff1a;openpyxl 2、库的安装3、核心代码4、完整代码5、最快的方法(50万行44秒)——表头其余单元格都修改样式总结 专栏导读 &#x1f338; 欢迎来到Python办公自动化专栏—Python处理办公问题&#xff0c;解放您的双手 &#x1f3f3;️‍…

【C#】书籍信息的添加、修改、查询、删除

文章目录 一、简介二、程序功能2.1 Book类属性&#xff1a;方法&#xff1a; 2.2 Program 类 三、方法&#xff1a;四、用户界面流程&#xff1a;五、程序代码六、运行效果 一、简介 简单的C#控制台应用程序&#xff0c;用于管理书籍信息。这个程序将允许用户添加、编辑、查看…

01-树莓派基本配置-基础配置配置

树莓派基本配置 文章目录 树莓派基本配置前言硬件准备树莓派刷机串口方式登录树莓派接入网络ssh方式登录树莓派更换国内源xrdp界面登录树莓派远程文件传输FileZilla 前言 树莓派是一款功能强大且价格实惠的小型计算机&#xff0c;非常适合作为学习编程、物联网项目、家庭自动化…

Netty面试内容整理-核心组件和概念

Netty 的核心组件和概念是理解其工作机制和开发网络应用的重要基础。以下是 Netty 中的核心组件和它们的功能: Channel ● 概念:Channel 是 Netty 中用于网络通信的基本抽象,表示一个连接,类似于 Java NIO 中的 SocketChannel 和 ServerSocketChannel。 ● 功能:

无人机探测:光电侦测技术详解

一、基本原理 光电识别技术是无人机追踪设备的核心&#xff0c;其原理主要基于光电转换和信号处理技术。光电识别设备通过光学系统收集目标的光学信息&#xff0c;如可见光、红外光等&#xff0c;并将其转换为电信号。这些电信号随后被处理和分析&#xff0c;以实现对目标的识…

106.【C语言】数据结构之二叉树的三种递归遍历方式

目录 1.知识回顾 2.分析二叉树的三种遍历方式 1.总览 2.前序遍历 3.中序遍历 4.后序遍历 5.层序遍历 3.代码实现 1.准备工作 2.前序遍历函数PreOrder 测试结果 3.中序遍历函数InOrder 测试结果 4.后序遍历函数PostOrder 测试结果 4.底层分析 1.知识回顾 在99.…

go并发设计模式runner模式

go并发设计模式runner模式 真正运行的程序不可能是单线程运行的&#xff0c;go语言中最值得骄傲的就是CSP模型了&#xff0c;可以说go语言是CSP模型的实现。 假设现在有一个程序需要实现&#xff0c;这个程序有以下要求&#xff1a; 程序可以在分配的时间内完成工作&#xff0…

03-13、SpringCloud Alibaba第十三章,升级篇,服务降级、熔断和限流Sentinel

SpringCloud Alibaba第十三章&#xff0c;升级篇&#xff0c;服务降级、熔断和限流Sentinel 一、Sentinel概述 1、Sentinel是什么 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保…

【服务器问题】xshell 登录远程服务器卡住( 而 vscode 直接登录不上)

打开 xshell ssh 登录远程服务器&#xff1a;卡在下面这里&#xff0c;迟迟不继续 当 SSH 连接卡在 Connection established. 之后&#xff0c;但没有显示远程终端提示符时&#xff0c;这通常意味着连接已经成功建立&#xff0c;说明不是网络连接和服务器连接问题&#xff0c;…

图片预处理技术介绍4——降噪

图片预处理 大家好&#xff0c;我是阿赵。   这一篇将两种基础的降噪算法。   之前介绍过均值模糊和高斯模糊。如果从降噪的角度来说&#xff0c;模糊算法也算是降噪的一类&#xff0c;所以之前介绍的两种模糊可以称呼为均值降噪和高斯降噪。不过模糊算法对原来的图像特征的…

Axios:现代JavaScript HTTP客户端

在当今的Web开发中&#xff0c;与后端服务进行数据交换是必不可少的。Axios是一个基于Promise的HTTP客户端&#xff0c;用于浏览器和node.js&#xff0c;它提供了一个简单的API来执行HTTP请求。本文将介绍Axios的基本概念、优势、安装方法、基本用法以及如何使用Axios下载文件。…

Linux 网络编程之TCP套接字

前言 上一期我们对UDP套接字进行了介绍并实现了简单的UDP网络程序&#xff0c;本期我们来介绍TCP套接字&#xff0c;以及实现简单的TCP网络程序&#xff01; &#x1f389;目录 前言 1、TCP 套接字API详解 1.1 socket 1.2 bind 1.3 listen 1.4 accept 1.5 connect 2、…

AI/ML 基础知识与常用术语全解析

目录 一.引言 二.AI/ML 基础知识 1.人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09; (1).定义 (2).发展历程 (3).应用领域 2.机器学习&#xff08;Machine Learning&#xff0c;ML&#xff09; (1).定义 (2).学习方式 ①.监督学习 ②.无监督…

计算机网络常见面试题总结(上)

计算机网络基础 网络分层模型 OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f; OSI 七层模型 是国际标准化组织提出的一个网络分层模型&#xff0c;其大体结构以及每一层提供的功能如下图所示&#xff1a; 每一层都专注做一件事情&#xff0c;并且每一层都需…

jupyter-lab 环境构建

我平时用来调试各种代码的。 创建环境&#xff0c;安装库 conda create --name jupyterlab python3.12 -y conda activate jupyterlab conda install -c conda-forge jupyterlab nodejs之前用的是3.10的&#xff0c;但是最近安装的时候&#xff0c;发现3.10的python里面的jup…

蓝桥杯准备训练(lesson1,c++方向)

前言 报名参加了蓝桥杯&#xff08;c&#xff09;方向的宝子们&#xff0c;今天我将与大家一起努力参赛&#xff0c;后序会与大家分享我的学习情况&#xff0c;我将从最基础的内容开始学习&#xff0c;带大家打好基础&#xff0c;在每节课后都会有练习题&#xff0c;刚开始的练…

Unity类银河战士恶魔城学习总结(P156 Audio Settings音频设置)

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址&#xff1a;https://www.udemy.com/course/2d-rpg-alexdev/ 本章节实现了音频的大小设置与保存加载 音频管理器 UI_VolumeSlider.cs 定义了 UI_VolumeSlider 类&#xff0c;用于处理与音频设置相关的…

如何为 ext2/ext3/ext4 文件系统的 /dev/centos/root 增加 800G 空间

如何为 ext2/ext3/ext4 文件系统的 /dev/centos/root 增加 800G 空间 一、引言二、检查当前磁盘和分区状态1. 使用 `df` 命令检查磁盘使用情况2. 使用 `lsblk` 命令查看分区结构3. 使用 `fdisk` 或 `parted` 命令查看详细的分区信息三、扩展逻辑卷(如果使用 LVM)1. 检查 LVM …

java调用ai模型:使用国产通义千问完成基于知识库的问答

整体介绍&#xff1a; 基于RAG&#xff08;Retrieval-Augmented Generation&#xff09;技术&#xff0c;可以实现一个高效的Java智能问答客服机器人。核心思路是将预先准备的问答QA文档&#xff08;例如Word格式文件&#xff09;导入系统&#xff0c;通过数据清洗、向量化处理…