Golang中使用了channel这个概念来实现了基于消息传递的并发,借用了CSP中channel与process这两个概念,其中process在Go语言中表现为Goroutine,各个Goroutine之间通过channel通讯来实现数据共享
在此篇文章中,我们首先会介绍Golang中channel的基本结构,以及结构中各个对象的用途。然后会介绍Golang中channel的接受、发送数据以及select的zero-case,one-case,multi-case等常见情况的大致的执行过程,抛弃运行中的大部分细节,大致了解整个过程对之后的源码阅读会有很大的帮助。最后会给出各个情况下源码执行的过程以及分析。
Channel基本结构
上图中的hchan结构体就是Golang中的channel在底层所对应的结构
- hchan中的buf, recvx, sendx这三个字段构成了RingBuffer(下文中会有介绍)这种数据结构,用来存储Channel中的数据
- closed用来标注该channel是否已经被关闭,我们执行关闭channel的操作的时候,并不是立马销毁hchan结构体,hchan结构体还需要继续存活来处理内部剩余的一些数据以及阻塞挂起在自身上的GoRoutine
- recvq与sendq分别是记录在接受channel数据时与在向channel中发送数据时阻塞的GoRoutine的队列,当GoRoutine在接受或者发送数据时发生阻塞时,会将这个发生阻塞的GoRoutine包装成sudog对象,放入对应的队列,等待相关资源可用之后再释放
- sudog中的elem是指向数据的一个指针,如果是发送消息的GoRoutine,elem指向要发送的数据,如果是接收消息的GoRoutine,elem指向数据将被读取到的位置
hchan中的RingBuffer
为了可以更好的理解在channel数据存取过程中底层RingBuffer的工作过程,我画了一幅图,图画的不是很好,但可以看懂其中的原理。首先我们给出一个容量为3的channel,图中的圆环代表channel的底层数组,r与s箭头分别代表recvx(读取索引)与sendx(发送索引)。在初始化状态中,r与s的值都指向数组头部。向channel中写入一个数据,会数据存到当前s指向的位置,并将s前移一位;读取channel中的一个数据,会读取当前r指向的位置上的数据,并将r前移一位,此时r与s重合,说明buffer中数据已空,此时再进行读取的操作会进入阻塞状态。随后向channle中写入两个数据,s会向前移两位,此时s指针若再向前移动一位则会与r“相遇”,说明buffer中数据已满,此时再进行发送的操作会进入阻塞状态
对Channel进行的各种操作
1.创建channel
- 分配hchan(从堆中分配,所有字段均为零值)
- 分配ring buffer(从堆中根据给定大小分配,如果是无缓冲channel则不需要这一步)
2.向有缓冲channel中发送数据(未满)
- 对channel上锁
- 将数据存入ring buffer
- 对channel解锁
3.从有缓冲channel中读取数据(非空)
- 对channel上锁
- 从ring buffer中读取数据
- 对channel解锁
4.向有缓冲channel中发送数据(已满)
- 将执行操作的GoRoutine与要发送的数据封装为sudog对象
- sudog对象加入sendq队列
- gopark(通知GoRoutine的调度器放弃这个GoRoutine,继续循环调度其他GoRoutine)
若此时出现一个新的接受方接收channel中的数据
- sendq队列中队首的sudog出队
- 将sudog中的数据放入ring buffer
- goready(将GoRoutine重新放入调度队列,等待被调度器调度)
5.从有缓冲channel中读取数据(空)
- 将执行操作的GoRoutine与接收数据的位置封装为sudog对象
- sudog对象加入recvq队列
- gopark
若此时出现一个新的发送方向channel中发送数据
- recvq队列中对手的sudog出队
- 直接将发送方的要发送的数据写入刚刚出队的sudog中的元素位置处
- goready
6.关闭channel
- 对channel加锁
- hchan中的closed字段修改为true
- ready所有sendq与recvq中的sudog
- 解锁
7.从已经关闭的channel中读取数据
- 查看底层的ring buffer中是否为空
- 如果为空,清空ring buffer
- 如果不为空,继续从buffer中读数据
channel相关操作的源码
channel各种操作的翻译
上述对channel的各种操作在编译时会被翻译成runtime包中的对应操作channel的函数,在阅读源码之前,我们要先了解一下各个操作所对应的函数,阅读源码的第一步是先要找到源码的位置
1 | make(chan interface{}, size) => runtime.makechan(interface{}, size) |
创建channel
1 | func makechan(t *chantype, size int) *hchan { |
发送数据
1 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { |
接受数据
1 | //channel接受数据分有ok返回值,与没有ok返回值的两个函数,分别为chanrecv1与chanrecv2,这两个函数都调用了chanrecv函数 |
Select的原理
Select的翻译工作
1 | //零case的select |