go chan底层分析
go chan底层分析
- 底层源码
- hchan
- makechan 方法
- 环形队列
- 阻塞机制
- 向管道写数据
- 流程图
- 源码
- 从管道读数据
- 流程图
- 源码
- 关闭通道
底层源码
hchan
type hchan struct {qcount uint // 当前队列中剩余元素个数dataqsiz uint // 环形队列长度,即可以存放的元素个数,也就是通道的缓冲区大小buf unsafe.Pointer // 是一个指向环形队列的指针elemsize uint16 // 存储通道中每个元素的大小,单位是字节。closed uint32 // 标识关闭状态elemtype *_type // 元素类型sendx uint // 队列下标,指示元素写入时存放到队列中的位置recvx uint // 队列下标,指示元素从队列的该位置读出recvq waitq // 等待读消息的goroutine队列sendq waitq // 等待写消息的goroutine队列lock mutex // 互斥锁,chan不允许并发读写
}
读消息协程队列(recvq)
和 写消息协程队列(sendq)
分别是接收(<- channel))和 发送(channel <- xxx)的 协程 抽象出来的结构体(sudog)的队列,是个双向链表。
type waitq struct {first *sudoglast *sudog
}
makechan 方法
makechan 是一个内部方法,用于创建通道。它位于 src/runtime 目录下,负责通道内存的分配、初始化通道结构体等操作。makechan 方法是由 Go 运行时调用的,它不会直接出现在普通用户代码中,而是与 Go 的低级运行时管理密切相关。
创建一个管道会在 heap
中实例化一个 hchan
对象,并返回这个对象的指针。
func makechan(t *chantype, size int) *hchan {// t 是由 Go 编译器在编译时生成的。// elem 是通道元素的类型描述符(*chantype),它包含了关于通道元素类型的各种信息。// elem.Size_ 是 elem 结构体中的字段,表示通道元素类型的大小(以字节为单位)。// 例如:如果通道的元素类型是 int,那么 elem.Size_ 就是 int 类型的大小(通常是 4 字节或 8 字节,具体取决于平台)。大小是编译时确定的,并通过 elem.Size_ 字段存储在 chantype 中。elem := t.Elem// 1.元素大小检查:查通道中元素的大小是否超过了 64KB(1 << 16)。通道中每个元素的大小不能超过 64KB,超出此限制会导致不合法的元素类型。if elem.Size_ >= 1<<16 {throw("makechan: invalid channel element type")}// 2.对齐条件检查:检查 hchan 结构体的大小和元素的对齐要求是否符合系统的对齐规则。如果不符合,将抛出异常。if hchanSize%maxAlign != 0 || elem.Align_ > maxAlign {throw("makechan: bad alignment")}// 3.计算所需内存:计算通道缓冲区所需的内存大小。elem.Size_ 是单个元素的大小,size 是通道的大小(即缓冲区中的元素个数)。如果计算结果溢出或者超出了最大分配内存限制,代码会抛出异常。mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))if overflow || mem > maxAlloc-hchanSize || size < 0 {panic(plainError("makechan: size out of range"))}// 4.内存分配:(mallocgc 函数,它会在 Go 的垃圾回收器中分配内存。mallocgc 会根据需要将内存注册到垃圾回收系统,并处理内存的初始化)var c *hchanswitch {// mem == 0:如果元素大小为 0(即元素为零字节),则只分配 hchan 结构体所需的内存。case mem == 0:c = (*hchan)(mallocgc(hchanSize, nil, true))c.buf = c.raceaddr()// elem.PtrBytes == 0:如果元素类型不包含指针(即元素是简单数据类型),则通道和缓冲区内存会一次性分配。case elem.PtrBytes == 0:c = (*hchan)(mallocgc(hchanSize+mem, nil, true))c.buf = add(unsafe.Pointer(c), hchanSize)// 其他情况:如果元素类型包含指针,则首先为 hchan 分配内存,然后单独为元素数据(缓冲区)分配内存。default:c = new(hchan)c.buf = mallocgc(mem, elem, true)}// 5.初始化通道信息c.elemsize = uint16(elem.Size_) // 存储单个元素的大小c.elemtype = elem // 存储元素类型的描述信息c.dataqsiz = uint(size) // 存储缓冲区的大小(即通道中可以存储的元素数量)lockInit(&c.lock, lockRankHchan) // 初始化 hchan 结构体中的锁,用于保证并发操作时的同步// 6.调试输出:如果启用了调试模式,Go 运行时会打印通道创建的信息,用于调试。if debugChan {print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")}return c
}
环形队列
chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个元素的channel示意图:
- dataqsiz 表示了队列长度为6,即可缓存6个元素;
- buf 指向队列的内存;
- qcount 表示队列中还有两个元素;
- sendx 表示后续写入的数据存储的位置,取值[0, 6);
- recvx 表示从该位置读取数据, 取值[0, 6);
阻塞机制
-
一个协程向一个 管道读数据,如果管道缓冲区为空或者没有缓冲区,当前的协程会被加入到
读消息协程队列(recvq)
中,并且被挂起来,直到对应的条件满足时(例如缓冲区有数据),它会被唤醒并继续执行; -
一个协程向一个管道写数据,如果管道缓冲区已经满了或者没有缓冲区,当前的协程会被加入到
写消息协程队列(sendq)
中,并且被挂起来,直到对应的条件满足时(例如缓冲区有空间),它会被唤醒并继续执行。
注意:处于等待队列中的协程会在其他协程操作管道时被唤醒,具体如下,
- 因读阻塞的协程会被向管道写人数据的协程唤醒。
- 因写阻塞的协程会被从管道读数据的协程唤醒。
注意:一般不会出现
读消息协程队列(recvq)
和写消息协程队列(sendq)
中同时有协程排队的情况,只有一个例外,那就是同一个协程使用 select 语句向管道一边写数据、一边读数据,此时协程会分别位于两个等待队列中。
向管道写数据
向一个管道中写数据的过程如下:
- 如果缓冲区中有空余位置,则将数据写人缓冲区,结束写消息过程。
- 如果缓冲区中没有空余位置,则将
写消息协程
加人写消息协程队列(sendq)
,进入睡眠并等待被读协程
唤醒。 - 特殊情况:直接将准备写的数据传递给
读消息协程队列(recvq)
。具体如下,当读消息协程队列(recvq)
中有协程等待时,会将准备写的数据直接传递给读消息协程队列(recvq)
中的第一个读消息协程
,而不需要通过缓冲区。这是一个优化手段,避免了无谓的缓冲区操作。
流程图
注意:写消息的时候,如果是无缓冲管道,直接写消息,而且
读消息协程队列
中没有协程,这个时候就会直接阻塞报错,要确保在写消息前读消息协程队列
不为空。
源码
特殊情况:
直接将数据传递给读消息协程队列(recvq)
。如果读消息协程队列(recvq)
中有读消息协程
等待接收数据,那么直接将发送的数据传递给读消息协程
,跳过缓冲区。
阻塞操作:
如果管道缓冲区已满,并且没有读消息协程队列(recvq)
中的读消息协程
在等待,则发送操作会被阻塞,直到有空间可以写入数据或者接收协程完成了数据接收。
协程阻塞和唤醒机制:
在写消息协程
被阻塞时(即没有缓冲区了,而且读消息协程队列(recvq)
中为空),程序会将其添加到写消息协程队列(sendq)
中,并通过 gopark 将其置于等待状态。
从管道读数据
从一个管道读数据的简单过程如下:
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读消息过程。
- 如果缓冲区中没有数据,则将
读消息协程
加入读消息协程队列(recvq)
,进入睡眠并等待被写消息协程
唤醒。
同样,在实现时有个小技巧:如果 写消息协程队列(sendq)
不为空,且没有缓冲区,那么此时将直接从 写消息协程队列(sendq)
的第一个写消息协程
中获取数据。
流程图
源码
通道已关闭且没有数据:如果通道已关闭,且缓冲区没有数据(
qcount == 0
),接收者会清理数据(如果存在的话),释放锁,然后返回。通道已关闭但缓冲区有数据:如果通道已关闭,但缓冲区中有数据,接收者可以正常接收数据。
通道未关闭且有等待发送的数据:如果通道未关闭,并且
写消息协程队列(sendq)
中有等待写的消息,接收者将直接从写消息协程队列(sendq)
中获取数据。
略…
关闭通道
关闭管道时会把 读消息协程队列(recvq)
中的 读消息协程
全部唤醒,这些协程获取的数据都为对应类型的零值。同时还会把 写消息协程队列(sendq)
中的 写消息协程
全部唤醒,但这些协程会触发 panic。
除此之外,其他会触发 panic 的操作还有:
- 关闭值为nil 的管道。
- 关闭已经被关闭的管道。
- 向已经关闭的管道写入数据。
参考:
go专家编程
图解Go的channel底层实现
【幼麟实验室】Golang合辑
相关文章:

go chan底层分析
go chan底层分析 底层源码hchanmakechan 方法 环形队列阻塞机制向管道写数据流程图源码 从管道读数据流程图源码 关闭通道 底层源码 hchan type hchan struct {qcount uint // 当前队列中剩余元素个数dataqsiz uint // 环形队列长度,即可以…...

idea上git log面板的使用
文章目录 各种颜色含义具体的文件的颜色标签颜色🏷️ 节点和路线 各种颜色含义 具体的文件的颜色 红色:表示还没有 git add 提交到暂存区绿色:表示已经 git add 过,但是从来没有 commit 过蓝色:表示文件有过改动 标…...

WOA-Transformer鲸鱼算法优化编码器时间序列预测(Matlab实现)
WOA-Transformer鲸鱼算法优化编码器时间序列预测(Matlab实现) 目录 WOA-Transformer鲸鱼算法优化编码器时间序列预测(Matlab实现)预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现WOA-Transformer鲸鱼算法优化编…...

dock 制作 python环境
报错 :Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 解决方法 配置加速地址 vim /etc/docker/daemon.json 添加以下内容 { "registry-mirror…...

2025第3周 | json-server的基本使用
目录 1. json-server是什么?2. json-server怎么用?2.1 安装2.2 创建db.json2.3 启动服务2.4 查看效果 3. 前端进行模拟交互3.1 创建demo.html3.2 创建demo.js 2025,做想做的事,读想读的书,持续学习,自律生活…...

Autodl转发端口,在本地机器上运行Autodl服务器中的ipynb文件
通过 SSH 隧道将远程端口转发到本地机器 输入服务器示例的SSH指令和密码,将远程的6006端口代理到本地 在服务器终端,激活conda虚拟环境 conda activate posecnnexport PYOPENGL_PLATFORMegljupyter notebook --no-browser --port6006 --allow-root从…...

flutter Get GetMiddleware 中间件不起作用问题
当使用 get: ^5.0.0-release-candidate-9.2.1最新版本时,中间件GetMiddleware各种教程都是让我们在redirect中实现,比如: overrideRouteSettings? redirect(String? route) {return RouteSettings(name: "/companyAuthIndexPage"…...

RabbitMQ(三)
RabbitMQ中的各模式及其用法 工作队列模式一、生产者代码1、封装工具类2、编写代码3、发送消息效果 二、消费者代码1、编写代码2、运行效果 发布订阅模式一、生产者代码二、消费者代码1、消费者1号2、消费者2号 三、运行效果四、小结 路由模式一、生产者代码二、消费者代码1、消…...

【Python】Python之locust压测教程+从0到1demo:基础轻量级压测实战(1)
文章目录 一、什么是Locust二、Locust 架构组成三、实战 Demo准备一个可调用的接口编写一个接口测试用例编写一个性能测试用例执行性能测试用例代码1、通过 Web UI 执行(GUI模式)2、通过命令行执行(非GUI模式) 小知识:…...

【JavaScript】基础内容,HTML如何引用JavaScript, JS 常用的数据类型
HTML 嵌入 Javascript 的方式 引入外部 js 文件 <head> <script Language "javaScript" src"index.js"/> </head>内部声明 <head> <script language"javascript">function hello(){alert("hello word&qu…...

vue使用自动化导入api插件unplugin-auto-import,避免频繁手动导入
unplugin-auto-import是一个现代的自动导入插件,旨在简化前端开发中的导入过程,减少手动导入的繁琐工作,提升开发效率。它支持多种构建工具,包括Vite、Webpack、Rollup和esbuild,并且可以与TypeScript配合使用&…...

在 C# 中的Lambda 表达式
在 C# 中,Lambda 表达式是用来定义匿名函数的一种简洁方式,通常用于简化代码,尤其是在 LINQ 查询、事件处理或方法作为参数的场景中。Lambda 表达式的语法如下: 基本语法 (parameters) > expression_or_statement_blockparam…...

奉加微PHY6230兼容性:部分手机不兼容
从事嵌入式单片机的工作算是符合我个人兴趣爱好的,当面对一个新的芯片我即想把芯片尽快搞懂完成项目赚钱,也想着能够把自己遇到的坑和注意事项记录下来,即方便自己后面查阅也可以分享给大家,这是一种冲动,但是这个或许并不是原厂希望的,尽管这样有可能会牺牲一些时间也有哪天原…...

32单片机综合应用案例——基于GPS的车辆追踪器(三)(内附详细代码讲解!!!)
困难不会永远存在,只要你勇于面对,坚持努力,就一定能够战胜一切困难。每一次挑战都是一次成长的机会,不要害怕失败,失败是成功之母。只有经历过失败,你才能更加明白自己的不足,并不断改进自己&a…...

45_Lua模块与包
Lua中的模块系统是该语言的一个重要特性,它允许开发者将代码分割成更小、更易于管理的部分。通过使用模块,你可以创建可重用的代码片段,并且可以降低代码间的耦合度。下面我将详细介绍Lua模块的基本概念、语法以及一些实际案例。 1.Lua模块 1.1 模块的基本概念 从Lua 5.1…...

深度学习电影推荐-CNN算法
文章目录 前言视频演示效果1.数据集环境配置安装教程与资源说明1.1 ML-1M 数据集概述1.1.1数据集内容1.1.2. 数据集规模1.1.3. 数据特点1.1.4. 文件格式1.1.5. 应用场景 2.模型架构3.推荐实现3.1 用户数据3.2 电影数据3.3 评分数据3.4 数据预处理3.5实现数据预处理3.6 加载数据…...

【Git 】探索 Git 的魔法——git am 与补丁文件的故事
在日常的开发协作中,你可能会遇到这样的场景:某位热心的小伙伴发来一份 .patch 文件,让你把某个问题修复合并到项目中。如果你不知道如何优雅地接收并应用这份补丁,那么这篇文章就是为你准备的!让我们一起揭开 Git 的“…...

G1原理—5.G1垃圾回收过程之Mixed GC
大纲 1.Mixed GC混合回收是什么 2.YGC可作为Mixed GC的初始标记阶段 3.Mixed GC并发标记算法详解(一) 4.Mixed GC并发标记算法详解(二) 5.Mixed GC并发标记算法详解(三) 6.并发标记的三色标记法 7.三色标记法如何解决错标漏标问题 8.SATB如何解决错标漏标问题 9.重新梳…...

机器人传动力系统介绍
电驱动系统 无框力矩电机减速器:优点是功率密度高,可在有限空间产生大扭矩,使机器人关节运动有力灵活,如人形机器人四肢运动。缺点是系统复杂,成本高,减速器会降低传动效率.空心杯电机行星滚柱丝杆&#x…...

1161 Merging Linked Lists (25)
Given two singly linked lists L1a1→a2→⋯→an−1→an and L2b1→b2→⋯→bm−1→bm. If n≥2m, you are supposed to reverse and merge the shorter one into the longer one to obtain a list like a1→a2→bm→a3→a4→bm−1⋯. For ex…...

内联变量(inline variables):在多个文件中共享全局常量
在 C17 中,引入了 内联变量(inline variables) 的概念,可以用于在多个文件中共享全局常量。内联变量允许在头文件中定义变量,而不会导致链接错误(如重复定义)。这种方式非常适合用于定义跨多个文…...

Jmeter进行http接口并发测试
目录: 1、Jmeter设置(1)设置请求并发数(2)设置请求地址以及参数(3)添加结果数 2、启动看结果 1、Jmeter设置 (1)设置请求并发数 (2)设置请求地址…...

力扣解题汇总_JAVA
文章目录 数学_简单13_罗马数字转整数66_ 加一9_回文数70_爬楼梯69_x的平方根509_斐波那契数列2235_两整数相加67_二进制求和415_字符串相加2413_最小偶倍数2469_温度转换704_二分查找(重点) 数组_简单1_两数之和88_合并两个有序数组 链表_简单21_合并两个有序链表203_移除链表…...

ubuntu下安装编译cmake,grpc与protobuf
文章目录 install cmakeinstall grpcinstall protobuf注 install cmake sudo apt-get install -y g make libssl-devcd third_party/cmake-3.17.2./configuresudo make && make installcmake --version install grpc $ sudo apt-get install -y build-essential auto…...

SQL Prompt 插件
SQL Prompt 插件 注:SQL Prompt插件提供智能代码补全、SQL格式化、代码自动提示和快捷输入等功能,非常方便,可以自行去尝试体会。 1、问题 SSMS(SQL Server Management Studio)是SQL Server自带的管理工具,…...

知识图谱抽取分析中,如何做好实体对齐?
在知识图谱抽取分析中,实体对齐是将不同知识图谱中的相同实体映射到同一表示空间的关键步骤。为了做好实体对齐,可以参考以下方法和策略: 基于表示学习的方法: 使用知识图谱嵌入技术,如TransE、GCN等,将实体…...

【Python通过UDP协议传输视频数据】(界面识别)
提示:界面识别项目 前言 随着网络通信技术的发展,视频数据的实时传输在各种场景中得到了广泛应用。UDP(User Datagram Protocol)作为一种无连接的协议,凭借其低延迟、高效率的特性,在实时性要求较高的视频…...

【伪随机数】关于排序算法自测如何生成随机数而引发的……
以 Random 开始 可能一开始,你只是写到了排序算法如何生成随机数 public static void main(String[] args) {Random random new Random();int[] nums new int[10];for (int i 0; i < nums.length; i) {nums[i] random.nextInt(100);}System.out.println(&q…...

核密度估计(Kernel Density Estimation, KDE)是一种非参数统计方法
一、核密度估计 核密度估计(Kernel Density Estimation, KDE)是一种非参数统计方法,用于估计随机变量的概率密度函数。它通过将每个数据点周围的核函数叠加,生成平滑的密度曲线。以下是其核心要点: 1. 基本概念 非参…...

【k8s面试题2025】2、练气初期
在练气初期,灵气还比较稀薄,只能勉强在体内运转几个周天。 文章目录 简述k8s静态pod为 Kubernetes 集群移除新节点:为 K8s 集群添加新节点Kubernetes 中 Pod 的调度流程 简述k8s静态pod 定义 静态Pod是一种特殊类型的Pod,它是由ku…...