Go channel
礼物说设计原理
Go 语言中最常见的、也是经常被人提及的设计模式就是:不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。在很多主流的编程语言中,多个线程传递数据的方式一般都是共享内存,为了解决线程竞争,我们需要限制同一时间能够读写这些变量的线程数量,然而这与 Go 语言鼓励的设计并不相同。
type hchan struct {
uint
qcount uint
dataqsiz .Pointer
buf unsafeuint16
elemsize uint32
closed *_type
elemtype uint
sendx uint
recvx
recvq waitq
sendq waitq
lock mutex}
type waitq struct {
*sudog
first *sudog
last }
- qcount — Channel 中的元素个数;
- dataqsiz — Channel 中的循环队列的长度;
- buf — Channel 的缓冲区数据指针;
- sendx — Channel 的发送操作处理到的位置;
- recvx — Channel 的接收操作处理到的位置;
- sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,这些等待队列使用双向链表
runtime.waitq
表示;
发送数据 ch <- i
在发送数据的逻辑执行之前会先为当前 Channel 加锁,防止多个线程并发修改数据。如果 Channel 已经关闭,那么向该 Channel 发送数据时会报 “send on closed channel” 错误并中止程序。
- 当存在等待的接收者时,通过 runtime.send 直接将数据发送给阻塞的接收者;
- 调用
runtime.sendDirect
将发送的数据直接拷贝到 x = <-c 表达式中变量 x 所在的内存地址上; - 调用
runtime.goready
将等待接收数据的 Goroutine 标记成可运行状态 Grunnable 并把该 Goroutine 放到发送方所在的处理器的 runnext 上等待执行,该处理器在下一次调度时会立刻唤醒数据的接收方;
- 调用
- 当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
- 使用
runtime.chanbuf
计算出下一个可以存储数据的位置,然后通过runtime.typedmemmove
将发送的数据拷贝到缓冲区中并增加 sendx 索引和 qcount 计数器。
- 使用
- 当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据;
接收数据 i <- ch
如果当前 Channel 已经被关闭并且缓冲区中不存在任何数据,那么会清除ep
指针中的数据并立刻返回。
- 当存在等待的发送者时,通过
runtime.recv
从阻塞的发送者或者缓冲区中获取数据;- 如果 Channel 不存在缓冲区;
- 调用
runtime.recvDirect
将 Channel 发送队列中 Goroutine 存储的elem
数据拷贝到目标内存地址中;
- 调用
- 如果 Channel 存在缓冲区;
- 将队列中的数据拷贝到接收方的内存地址;
- 将发送队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;
- 无论发生哪种情况,运行时都会调用
runtime.goready
将当前处理器的runnext
设置成发送数据的 Goroutine,在调度器下一次调度时将阻塞的发送方唤醒。
- 如果 Channel 不存在缓冲区;
- 当缓冲区存在数据时,从 Channel 的缓冲区中接收数据;
- 当 Channel 的缓冲区中已经包含数据时,从 Channel 中接收数据会直接从缓冲区中
recvx
的索引位置中取出数据进行处理
- 当 Channel 的缓冲区中已经包含数据时,从 Channel 中接收数据会直接从缓冲区中
- 当缓冲区中不存在数据时,等待其他 Goroutine 向 Channel 发送数据;
- 在正常的接收场景中,我们会使用
runtime.sudog
将当前 Goroutine 包装成一个处于等待状态的 Goroutine 并将其加入到接收队列中。 - 完成入队之后,上述代码还会调用
runtime.goparkunlock
立刻触发 Goroutine 的调度,让出处理器的使用权并等待调度器的调度。
- 在正常的接收场景中,我们会使用
关闭管道
当Channel
是一个空指针或者已经被关闭时,Go语言运行时都会直接崩溃并抛出异常;
将recvq
和sendq
两个队列中的数据加入到 Goroutine 列表gList中,与此同时该函数会清除所有
runtime.sudog`上未被处理的元素.
该函数在最后会为所有被阻塞的 Goroutine 调用runtime.goready
触发调度。