您现在的位置是:网站首页> 编程资料编程资料
GO语言中Chan实现原理的示例详解_Golang_
2023-12-26
164人已围观
简介 GO语言中Chan实现原理的示例详解_Golang_
GO 中 Chan 实现原理分享
嗨,我是小魔童哪吒,还记得咱们之前分享过GO 通道 和sync包的使用吗?咱们来回顾一下
- 分享了通道是什么,通道的种类
- 无缓冲,有缓冲,单向通道具体对应什么
- 对于通道的具体实践
- 分享了关于通道的异常情况整理
- 简单分享了sync包的使用
要是对上述内容还有点兴趣的话,欢迎查看文章 GO通道和 sync 包的分享
chan 是什么
是一种特殊的类型,是连接并发goroutine的管道
channel 通道是可以让一个 goroutine 协程发送特定值到另一个 goroutine 协程的通信机制。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序,这一点和管道是一样的
一个协程从通道的一头放入数据,另一个协程从通道的另一头读出数据
每一个通道都是一个具体类型的导管,声明 channel 的时候需要为其指定元素类型。
本篇文章主要是分享关于通道的实现原理,关于通道的使用,可以查看文章 GO通道和 sync 包的分享 ,这里有详细的说明
GO 中 Chan 的底层数据结构
了解每一个组件或者每一个数据类型的实现原理,咱们都会去看源码中的数据结构是如何设计的
同样,我们一起来看看 GO 的 Chan 的数据结构
GO 的 Chan 的源码实现是在 : src/runtime/chan.go

type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G's status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex }hchan 是实现通道的核心数据结构,对应的成员也是不少,咱们根据源码注释一个参数一个参数的来看看
| tag | 说明 |
|---|---|
| qcount | 当前的队列,剩余元素个数 |
| dataqsiz | 环形队列可以存放的元素个数,也就是环形队列的长度 |
| buf | 指针,指向环形队列 |
| elemsize | 指的的队列中每个元素的大小 |
| closed | 具体标识关闭的状态 |
| elemtype | 见名知意,元素的类型 |
| sendx | 发送队列的下标,向队列中写入数据的时候,存放在队列中的位置 |
| recvx | 接受队列的下标,从队列的 这个位置开始读取数据 |
| recvq | 协程队列,等待读取消息的协程队列 |
| sendq | 协程队列,等待发送消息的协程队列 |
| lock | 互斥锁,在 chan 中,不可以并发的读写数据 |
根据上面的参数,我们或多或少就可以知道 GO 中的通道实现原理设计了哪些知识点:
- 指针
- 环形队列
- 协程
- 互斥锁
我们顺便再来看看上述成员的协程队列 waitq 对应的是啥样的数据结构
type waitq struct { first *sudog last *sudog }sudog 结构是在 src/runtime/runtime2.go中 ,咱们顺便多学一手
// sudog represents a g in a wait list, such as for sending/receiving // on a channel. type sudog struct { // The following fields are protected by the hchan.lock of the // channel this sudog is blocking on. shrinkstack depends on // this for sudogs involved in channel ops. g *g next *sudog prev *sudog elem unsafe.Pointer // data element (may point to stack) // The following fields are never accessed concurrently. // For channels, waitlink is only accessed by g. // For semaphores, all fields (including the ones above) // are only accessed when holding a semaRoot lock. acquiretime int64 releasetime int64 ticket uint32 // isSelect indicates g is participating in a select, so // g.selectDone must be CAS'd to win the wake-up race. isSelect bool // success indicates whether communication over channel c // succeeded. It is true if the goroutine was awoken because a // value was delivered over channel c, and false if awoken // because c was closed. success bool parent *sudog // semaRoot binary tree waitlink *sudog // g.waiting list or semaRoot waittail *sudog // semaRoot c *hchan // channel }根据源码注释,咱们大致知道sudog 是干啥的
Sudog表示等待列表中的 g,例如在一个通道上发送/接收
Sudog是很必要的,因为g↔synchronization对象关系是多对多
一个 g 可能在很多等候队列上,所以一个 g 可能有很多sudogs
而且许多 g 可能在等待同一个同步对象,所以一个对象可能有许多sudogs
咱们抓住主要矛盾
Sudog的数据结构,主要的东西就是一个 g 和一个 elem ,
g,上面有说到他和 Sudog的对应关系
无论是读通道还是写通道,都会需要 elem
读通道
数据会从hchan的队列中,拷贝到sudog的elem中
写通道
与读通道类似,是将数据从 sudog 的elem处拷贝到hchan的队列中
咱们来画个图看看

此处咱们画一个 hchan的结构,主要画一下 recvq等待读取消息的协程队列,此处的队列,实际上就是用链表来实现的
recvq会对应到 waitq结构,waitq 分为first头结点 和 last尾节点 结构分别是 sudog
sudog里面 elem存放具体的数据,next 指针指向下一个 sudog,直到指到last 的 sudog
通过上述的,应该就能明白 GO 中的 chan 基本结构了吧
咱来再来详细看看 hchan 中其他参数都具体是啥意思
dataqsiz对应的环形队列是啥样的- 写
sendq和 读recvq等待队列是啥样的 elemtype元素类型信息又是啥
dataqsiz 对应的环形队列是啥样的
环形队列,故名思议就是 一个首尾连接,成环状的队列
GO 中的 chan内部的环形队列,主要作用是作为缓冲区
这个环形队列的长度,我们在创建队列的时候, 也就是创建 hchan 结构的时候,就已经指定好了的
就是 dataqsiz ,环形队列的长度
咱们画个图清醒一下

上图需要表达的意思是这个样子的,上述的队列是循环队列,默认首尾连接哦:
- dataqsiz 表示 循环队列的长度是 8 个
- qcount 表示 当前队列中有 5 个元素
- buf 是指针,指向循环队列头
- sendx 是发送队列的下标,这里为 1 ,则指向队列的第 2 个区域 ,这个参数可选范围是 [0 , 8)
- recvx 是接收队列的下标,这里为 4 ,则指向的是 队列的第 5 个区域进行读取数据
这里顺带提一下,hchan 中读取数据还是写入数据,都是需要去拿 lock 互斥锁的,同一个通道,在同一个时刻只能允许一个协程进行读写
写 sendq和 读 recvq 等待队列是啥样的
hchan 结构中的 2 个协程队列,一个是用于读取数据,一个是用于发送数据,他们都是等待队列,我们来看看这个等待队列都是咋放数据上去的,分别有啥特性需要注意
当从通道中读取 或者 发送数据:
- 若通道的缓冲区为空,或者没有缓冲区,此时从通道中读取数据,则协程是会被阻塞的
- 若通道缓冲区为满,或者没有缓冲区,此时从通道中写数据,则协程仍然也会被阻塞
这些被阻塞的协程就会被放到等待队列中,按照读 和 写 的动作来进行分类为写 sendq和 读 recvq 队列
那么这些阻塞的协程,啥时候会被唤醒呢?
看过之前的文章 GO通道和 sync 包的分享,应该就能知道
我们在来回顾一下,这篇文章的表格,通道会存在的异常情况:
| channel 状态 | 未初始化的通道(nil) | 通道非空 | 通道是空的 | 通道满了 | 通道未满 |
|---|---|---|---|---|---|
| 接收数据 | 阻塞 | 接收数据 | 阻塞 | 接收数据 | 接收数据 |
| 发送数据 | 阻塞 | 发送数据 | 发送数据 | 阻塞 | 发送数据 |
| 关闭 | panic | 关闭通道成功 待数据读取完毕后 返回零值 | 关闭通道成功 直接返回零值 | 关闭通道成功 待数据读取完毕后 返回零值 | 关闭通道成功 待数据读取完毕后 返回零值 |
此时,我们就知道,具体什么时候被阻塞的协程会被唤醒了
- 因为读阻塞的协程,会被通道中的写入数据的协程唤醒,反之亦然
- 因为写阻塞的协程,也会被通道中读取数据的协程唤醒
elemtype元素类型信息又是啥
这个元素类型信息就不难理解了,对于我们使用通道,创建通道的时候我们需要填入通道中数据的类型,一个通道,只能写一种数据类型,指的就是这里的elemtype
另外 hchan 还有一个成员是elemsize,代表上述元素类型的占用空间大小
那么这俩成员有啥作用呢?
elemtype和elemsize就可以计算指定类型的数据占用空间大小了
前者用于在数据传递的过程中进行赋值
后者可以用来在环形队列中定位具体的元素
创建 chan 是咋实现的
我们再来瞅瞅 chan.go 的源码实现 ,看到源码中的 makechan 具体实现
func makechan(t *chantype, size int) *hchan { elem := t.elem // compiler checks this but be safe. if elem.size >= 1<<16 { throw("makechan: invalid channel element type") } if hchanSize%maxAlign != 0 || elem.align > maxAlign { throw("makechan: bad alignment") } mem, overflow := math.MulUintptr(elem.size, uintptr(size)) if overflow || mem > maxAlloc-hchanSize || size < 0 { panic(plainError("makechan: size out of range")) } // Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers. // buf points into the same allocation, elemtype is persistent. // SudoG's are referenced from their owning thread so they can't be collected. // TODO(dvyukov,rlh): Rethink when collector can move allocated objects. var c *hchan switch { case mem == 0: // Queue or element size is zero. c = (*hchan)(mallocgc(hchanSize, nil, true)) // Race detector uses this location for synchronization. c.buf = c.raceaddr() case elem.ptrdata == 0: // Elements do not contain pointers. // Allocate hchan and buf in one call. c = (*hc
相关内容
- GO语言中通道和sync包的使用教程分享_Golang_
- GO的锁和原子操作的示例详解_Golang_
- Golang调用FFmpeg转换视频流的实现_Golang_
- windows安装部署go超详细实战记录(实测有用!)_Golang_
- 瞅一眼就能学会的GO并发编程使用教程_Golang_
- Go实现共享库的方法_Golang_
- Win7系统ARP攻击什么意思?三种方法教你如何防御ARP攻击_windows7_Windows系列_操作系统_
- 放开那三国占矿为王活动介绍_手机游戏_游戏攻略_
- 放开那三国觉醒传承详细介绍_手机游戏_游戏攻略_
- 天天向上手游进击的祖玛无尽模式解锁攻略_手机游戏_游戏攻略_
点击排行
本栏推荐
