channel 在运行时的内部结构题的表示是 ,

这里还是主要分析源码,怎么用 channel,这里就不介绍了。

数据结构

hchan

比较简单的字段,就在上面注释了,其他字段这里再讲解一下:

bufsendxrecvx
elemsizech := make(chan int)
elemtype
sendx
recvx

索引起始是 0,理解如下:

recvqgoroutine
sendqgoroutine
lock

channel 在实现中依然使用到了锁,Go 所说的 使用通信来实现共享内存,实际上依然在底层使用锁来保证读写的原子性,实现出了一个面向数据流式的数据结构


recvqsendq

再来看看 sudog:

这里只展示了跟 channel 有关的字段。

sudog 结构是对 g 的再封装,传入 sendqrecvq 就是 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 个主要动作:

  1. 关闭 nil channel, 或者重复关闭,会 panic。
  2. 将 sendq、recvq 中的所有 协程 加入到 gList 链表中。
  3. 唤醒 gList 中 等待的 协程。


总结

好了,到这 通道的源码就分析完毕了。再总结一下就会发现,发送接收 数据的逻辑主要围绕 hchan结构体的 5 个字段来操作:

qcount、dataqsiz、buf、sendx、recvx 就是用来标识缓冲区中的数据,recvq、sendq 存放阻塞的协程。

发送/接收的处理逻辑依次是:

  1. 先直接发送/接收,即对应 sendq 和 recvq 中有数据。
  2. 将数据发送至缓冲区,或者从缓冲区读取数据。
  3. 发送/接收阻塞,将数据添加到 sendq 和 recvq 中,然后让出当前协程调度,等待唤醒。
recvq/sendq直接发送 / 直接接收接收阻塞/ 发送阻塞发送/接收sendq / recvq

再强调一下 buf、sendx、recvx 三个字段形成一个循环队列,用于充当缓冲区

最后需要注意的地方:

sendx = 0
recv = 0