channel 在运行时的内部结构题的表示是 ,
这里还是主要分析源码,怎么用 channel,这里就不介绍了。
数据结构
hchan
比较简单的字段,就在上面注释了,其他字段这里再讲解一下:
bufsendxrecvx
elemsizech := make(chan int)
elemtype
sendx
recvx
索引起始是 0,理解如下:
recvqgoroutine
sendqgoroutine
lock
channel 在实现中依然使用到了锁,Go 所说的 使用通信来实现共享内存,实际上依然在底层使用锁来保证读写的原子性,实现出了一个面向数据流式的数据结构
recvqsendq
再来看看 sudog:
这里只展示了跟 channel 有关的字段。
sudog 结构是对 g 的再封装,传入 sendq 和 recvq 就是 sudog 。
nextprev
elemch <- 1
isSelect == true
success == true
阅读了下面的内容会发现, sudog 除了充当链表外,主要作用就是保存当前的 g,以及要发送的信息( elem)。
创建通道
源码主要逻辑很简单:
主要的逻辑就是在堆上开辟空间,然后为 buf 赋值:
mem == 0mallocgcc.raceaddr()elem.ptrdata == 0hchanSize+memmath.MulUintptr(elem.size, uintptr(size))
elemsizeelemtypedataqsiz
发送数据
chansend1
chansend()true
一进来先上锁,然后判断是否关闭(我们往关闭了的通道发送数据会报错,判断逻辑就在这)。
然后就是处理逻辑,可以依次处理逻辑为:
- 直接发送
- 写入缓冲区
- 发送阻塞
然后我就依次来看看每个处理逻辑的详情。
直接发送
即recvq不为空的情况。 接收阻塞的时候,会向 recvq 添加 sudog。
c.recvq.dequeue()
然后就是 我们来看发送逻辑,代码并不多:
raceenabled == truego buildgo run-race
elemnil
sendDirect()
memmove()
goready()
goready()
写入缓冲区
dataqsize
chanbuf
即就是给 buf 添加数据。但这里不像我们 slice 直接 append 数据,而是先开辟一块数据内存,然后返回这个内存的地址。
qp := chanbuf(c, c.sendx)
typemmemmovememmove
buf
之后的逻辑就很简单了,看备注即可。
发送阻塞
发送阻塞比较麻烦,咱们看源码一点一点拆解:
getg()
acquireSudog()sudogcache
第三步:就是设置 sudog 待发送的相关信息。
enqueue()sendq
可以看到就是双向链表插入操作。
gopark()
我们发现 发送阻塞的数据,是保存到了 sudog 中,然后放进 channel 的等待发送链表 sendq 中。
唤醒后
最后就是被唤醒,即: 数据被接受了。逻辑比较简单,就是把当前协程上的关于 channel 的信息设置为 nil/false:
接收数据
chanrecv()
主要流程跟发送数据一样,对应三个:
- 直接接收
- 从缓冲区接收
- 接收阻塞
直接接收
即 sendq 不为空的情况。发送阻塞的时候,会向 sendq 添加 sudog。
可以看到逻辑代码完全跟着 发送 反着来的。
c.sendq.dequeue()sudog
recv
- 如果不是缓冲队列:
recvDirect()data := <-chan
if c.dataqsiz == 0else
在发送 1、2,的时候,数据直接是写进缓冲区的,逻辑可以看回看 写入缓冲区 章节。
而从缓冲区接受数据,一来不会进入 直接接收 的逻辑中,而是再下一节 接收缓冲区。
所以能到 直接接收 的情况,一定是有 发送阻塞 了,即 sendq 中有数据。 都 发送阻塞 了,说明缓冲区已经满了 即 Queue is full。
好咱们再看看源码就容易理解了:
qp := chanbuf(c, c.recvx)
i <- ch<- ch
3. 将缓冲区中的数据,将等待发送中的元素(sudog.elem) 拷贝到缓冲区索引为 0 的地方。
4. 接收位置+1
c.sendx = c.recvx
- 之后就没什么好说的,唤醒发送阻塞的协程
接收缓冲区
recvx
发现几乎和 队列满时的直接接收 逻辑一摸一样,只不过把 sendx 换成了 recvx
接收阻塞
看到这,相信大家会发现,接收的总体逻辑是和发送差不多的,只是反着来。接收阻塞 就更过分了,除了是
- c.recvq.enqueue(mysg) 等待接收的 sudog 入 recvq 队列
- gopark 的一些参数不同
然后就跟 发送阻塞 没两样:
唤醒逻辑就不写了,也没两样。
所以,发送/接收,搞懂一个,另一个就是镜像而已。
关闭通道
close()
看下来就 3 个主要动作:
- 关闭 nil channel, 或者重复关闭,会 panic。
- 将 sendq、recvq 中的所有 协程 加入到 gList 链表中。
- 唤醒 gList 中 等待的 协程。
总结
好了,到这 通道的源码就分析完毕了。再总结一下就会发现,发送 和 接收 数据的逻辑主要围绕 hchan结构体的 5 个字段来操作:
qcount、dataqsiz、buf、sendx、recvx 就是用来标识缓冲区中的数据,recvq、sendq 存放阻塞的协程。
发送/接收的处理逻辑依次是:
- 先直接发送/接收,即对应 sendq 和 recvq 中有数据。
- 将数据发送至缓冲区,或者从缓冲区读取数据。
- 发送/接收阻塞,将数据添加到 sendq 和 recvq 中,然后让出当前协程调度,等待唤醒。
recvq/sendq直接发送 / 直接接收接收阻塞/ 发送阻塞发送/接收sendq / recvq
再强调一下 buf、sendx、recvx 三个字段形成一个循环队列,用于充当缓冲区。
最后需要注意的地方:
sendx = 0
recv = 0