本文主要讨论 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
执行操作gopark
G
让出所占用的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
执行操作goready
sudog
出队channel
的协程发送等待队列recvq
,elem
进入g2
,g1
更改status
G
放入P
队列中,进入ready
状态,协程可运行
关闭
- 排除
panic
情况,亦即关闭空的信道和重复关闭信道- 遍历
sendq
和recvq
并出队,sudog
中的g
加入gList
等待唤醒,sudog
中的elem
置为nil
将被清除- 遍历
gList
并出栈,触发goready
调度