本文主要讨论 channel 底层的相关内容。
目录 Table of Contents
前情提要
为了实现
goroutine通信,有两种常见并发模型:
- 共享内存:使用共享内存方式,
Go中sync库包提供了多种同步的机制。- 消息队列:使用类似管道和消息队列的方式,各个并发单元数据相互独立,通过消息进行数据交换,
Go中channel类型模拟了这种同步的模式。让我们再一次来复读
Go社区的并发口号——“不要通过共享内存来通信,而应该通过通信来共享内存”。本文将讨论
Go并发编程中的通信桥梁channel的底层,如有错漏,欢迎指出 ;P
数据结构

1 | type hchan struct { |
Why Lock is Mutex?
channel的应用虽然体现了“通过通信来共享内存”的思想,但是channel的实现则遵循“通过锁作悲观并发控制”的做法。- 由于
buf和waitq均先进先出 (First In First Out),当goroutine并发地读写channel时,需要保证并发安全,使用互斥锁是很好理解的。
Why buf is Circylar Linked List?
- 任何节点可以作头节点开始遍历,只需要维护一个尾指针。
- 简化了
sendx和recvx的变化。- 使用
qcount而非空余单元消除循环链表的二义性,减少额外数据结构开销,可以直接返回buf链表长度。
Why waitq is Double Linked List?
- 既可以访问前驱节点,也可以访问后驱节点。
算法流程
总览

“不要通过共享内存来通信”:不要把数据放在共享区,通过限制同一时间的来访者,达到数据通信和并发安全的目的。
“应该通过通信来共享内存”:应该用信道作为中间者,通过读写该并发安全的第三方数据结构,实现数据通信和并发安全。
发送

channel 未满


- 加锁
- 拷贝
goroutine数据到channel中- 解锁
channel 已满


goroutine执行语句ch <- data,向channel的信道数据缓冲队列buf中写入拷贝数据(此时buf已满),触发goroutine scheduler执行操作goparkG让出所占用的M,进入waiting状态,协程阻塞- 已阻塞的
goroutine被抽象为sudog,入队channel的协程发送等待队列sendq

goroutine执行语句<- ch,从channel的信道数据缓冲队列buf中读取拷贝数据(此时buf非满),触发goroutine scheduler执行操作goready
sudog出队channel的协程发送等待队列sendq,elem进入buf,g更改status
G放入P队列中,进入ready状态,协程可运行
接收

channel 未空


- 加锁
- 拷贝
channel数据到goroutine中- 解锁
channel 已空


goroutine执行语句<- ch,向channel的信道数据缓冲队列buf中读取拷贝数据(此时buf已空),触发goroutine scheduler执行操作gopark
G让出所占用的M,进入waiting状态,协程阻塞已阻塞的
goroutine被抽象为sudog,入队channel的协程发送等待队列recvq

goroutine执行语句ch <- data,直接由g1拷贝数据到g2(不经过buf,不使用lock),触发goroutine scheduler执行操作goreadysudog出队channel的协程发送等待队列recvq,elem进入g2,g1更改statusG放入P队列中,进入ready状态,协程可运行
关闭

- 排除
panic情况,亦即关闭空的信道和重复关闭信道- 遍历
sendq和recvq并出队,sudog中的g加入gList等待唤醒,sudog中的elem置为nil将被清除- 遍历
gList并出栈,触发goready调度