go chan底层分析

ops/2025/1/20 19:47:30/

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);

img

阻塞机制

  1. 一个协程向一个 管道读数据,如果管道缓冲区为空或者没有缓冲区,当前的协程会被加入到 读消息协程队列(recvq)中,并且被挂起来,直到对应的条件满足时(例如缓冲区有数据),它会被唤醒并继续执行;

  2. 一个协程向一个管道写数据,如果管道缓冲区已经满了或者没有缓冲区,当前的协程会被加入到 写消息协程队列(sendq) 中,并且被挂起来,直到对应的条件满足时(例如缓冲区有空间),它会被唤醒并继续执行。

在这里插入图片描述

注意:处于等待队列中的协程会在其他协程操作管道时被唤醒,具体如下,

  1. 因读阻塞的协程会被向管道写人数据的协程唤醒。
  2. 因写阻塞的协程会被从管道读数据的协程唤醒。

注意:一般不会出现 读消息协程队列(recvq)写消息协程队列(sendq) 中同时有协程排队的情况,只有一个例外,那就是同一个协程使用 select 语句向管道一边写数据、一边读数据,此时协程会分别位于两个等待队列中。

向管道写数据

向一个管道中写数据的过程如下:

  1. 如果缓冲区中有空余位置,则将数据写人缓冲区,结束写消息过程。
  2. 如果缓冲区中没有空余位置,则将 写消息协程 加人 写消息协程队列(sendq),进入睡眠并等待被 读协程 唤醒。
  3. 特殊情况:直接将准备写的数据传递给 读消息协程队列(recvq) 。具体如下,当 读消息协程队列(recvq) 中有协程等待时,会将准备写的数据直接传递给 读消息协程队列(recvq) 中的第一个 读消息协程,而不需要通过缓冲区。这是一个优化手段,避免了无谓的缓冲区操作。

流程图

在这里插入图片描述

注意:写消息的时候,如果是无缓冲管道,直接写消息,而且 读消息协程队列 中没有协程,这个时候就会直接阻塞报错,要确保在写消息前 读消息协程队列 不为空。

源码

特殊情况
直接将数据传递给 读消息协程队列(recvq) 。如果 读消息协程队列(recvq) 中有 读消息协程 等待接收数据,那么直接将发送的数据传递给 读消息协程 ,跳过缓冲区。
在这里插入图片描述

阻塞操作
如果管道缓冲区已满,并且没有 读消息协程队列(recvq) 中的 读消息协程 在等待,则发送操作会被阻塞,直到有空间可以写入数据或者接收协程完成了数据接收。
在这里插入图片描述

协程阻塞和唤醒机制
写消息协程 被阻塞时(即没有缓冲区了,而且 读消息协程队列(recvq) 中为空),程序会将其添加到 写消息协程队列(sendq) 中,并通过 gopark 将其置于等待状态。
在这里插入图片描述

从管道读数据

从一个管道读数据的简单过程如下:

  1. 如果缓冲区中有数据,则从缓冲区取出数据,结束读消息过程。
  2. 如果缓冲区中没有数据,则将 读消息协程 加入 读消息协程队列(recvq),进入睡眠并等待被 写消息协程 唤醒。

同样,在实现时有个小技巧:如果 写消息协程队列(sendq) 不为空,且没有缓冲区,那么此时将直接从 写消息协程队列(sendq) 的第一个写消息协程 中获取数据。

流程图

在这里插入图片描述

源码

通道已关闭且没有数据:如果通道已关闭,且缓冲区没有数据(qcount == 0),接收者会清理数据(如果存在的话),释放锁,然后返回。

通道已关闭但缓冲区有数据:如果通道已关闭,但缓冲区中有数据,接收者可以正常接收数据。

通道未关闭且有等待发送的数据:如果通道未关闭,并且 写消息协程队列(sendq) 中有等待写的消息,接收者将直接从 写消息协程队列(sendq) 中获取数据。

在这里插入图片描述

略…

在这里插入图片描述

关闭通道

关闭管道时会把 读消息协程队列(recvq) 中的 读消息协程 全部唤醒,这些协程获取的数据都为对应类型的零值。同时还会把 写消息协程队列(sendq) 中的 写消息协程 全部唤醒,但这些协程会触发 panic。

除此之外,其他会触发 panic 的操作还有:

  1. 关闭值为nil 的管道。
  2. 关闭已经被关闭的管道。
  3. 向已经关闭的管道写入数据。

参考:
go专家编程

图解Go的channel底层实现

【幼麟实验室】Golang合辑


http://www.ppmy.cn/ops/151743.html

相关文章

C#如何获取电脑中的端口号和硬件信息

我们经常在使用一个串口软件的时候&#xff0c;发现软件中的端口号并不是普通的COM1&#xff0c;而是带有硬件信息的。 那么如果我们使用C#编写软件时候&#xff0c;如何获取到串口的硬件信息呢&#xff1f; 思路就是通过读取设备管理器里的条目来实现&#xff0c;我这里给大家…

仿 RabbitMQ 的消息队列1(实战项目)

一&#xff0c;消息队列的背景知识 我们以前学过阻塞队列&#xff0c;其实阻塞队列和消息队列的原理差不多。 在实际的后端开发中, 尤其是分布式系统⾥, 跨主机之间使⽤⽣产者消费者模型, 也是⾮常普遍的需求. 因此, 我们通常会把阻塞队列, 封装成⼀个独⽴的服务器程序, 并且赋…

【数据分享】1929-2024年全球站点的逐日平均气温数据(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、湿度等指标&#xff0c;其中又以气温指标最为常用&#xff01;说到气温数据&#xff0c;最详细的气温数据是具体到气象监测站点的气温数据&#xff01;本次我们为大家带来的就是具体到气象监…

[实现Rpc] 环境搭建 | JsonCpp | Mudou库 | callBack()

目录 1. 项目介绍 2. 技术选型 3. 开发环境和环境搭建 Ubuntu-22.04环境搭建 1. 安装 wget&#xff08;一般情况下默认会自带&#xff09; 2. 更换国内软件源 ① 备份原始 /etc/apt/sources.list 文件 ② 编辑软件源文件 ③ 更新软件包列表 3. 安装常用工具 3.1 安装…

Spring Boot 整合 Redis:提升应用性能的利器

Redis (Remote Dictionary Server) 是一款高性能的键值对存储数据库&#xff0c;它以内存存储为主&#xff0c;具有速度快、支持丰富的数据类型等特点&#xff0c;被广泛应用于缓存、会话管理、排行榜等场景。 Spring Boot 提供了对 Redis 的良好支持&#xff0c;使得我们可以轻…

yt-dlp脚本下载音频可选设置代理

import yt_dlp# 配置:是否使用代理 use_proxy = True # 设置为 False 可关闭代理# 代理地址 proxy_url = socks5://127.0.0.1:1089URLS = [https://www.bilibili.com/video/BV1WTktYcEcQ/?spm_id_from=333.1007.tianma.6-2-20.click&vd_source=dcb58f8fe1faf749f438620b…

《多模态语言模型的局限性与生态系统发展现状分析》

1. 多模态语言模型的主要局限性 推理能力问题 复杂推理任务表现不稳定图像理解深度差异大推理过程存在逻辑跳跃 技术实现挑战 视觉特征与语言理解的融合不完善训练数据和方法有限跨模态理解算法需优化 2. 生态系统的不成熟表现 评测标准问题 缺乏标准化评测框架性能评估方法…

复杂查询优化:避免 SQL 查询中的 N+1 查询问题

在 SQL 查询优化中&#xff0c;N1 查询问题是一个常见的性能问题&#xff0c;特别是在关系型数据库中。当你的查询不当时&#xff0c;可能会导致对数据库进行大量的额外查询&#xff0c;造成不必要的性能损耗。 什么是 N1 查询问题&#xff1f; N1 查询问题通常出现在一对多或…