乐趣区

深入理解channel:设计+源码

channel 是大家在 Go 中用的最频繁的特性,也是 Go 最自豪的特性之一,你有没有思考过:

Why:为什么要设计 channel?
What:channel 是什么样的?
How:channel 是如何实现的?

这篇文章,就来回答这 3 个问题。
channel 解决什么问题?
在 Golang 诞生之前,各编程语言都使用多线程进行编程,但多线程复杂、混乱、难以管理,对开发者并不是多么友好。
Golang 是 Google 为了解决高并发搜索而设计的,它们想使用简单的方式,高效解决并发问题,最后做成了,然后又把 Golang 开源了出来,以及到处推广,所以 Golang 自从诞生之初,就风风火火。
从 Golang 文档中,我们可以知道,为啥 Golang 设计了 channel,以及 channel 解决了什么问题?
Go Concurrency Patterns:
Concurrency is the key to designing high performance network services. Go’s concurrency primitives (goroutines and channels) provide a simple and efficient means of expressing concurrent execution. In this talk we see how tricky concurrency problems can be solved gracefully with simple Go code.
Golang 使用 goroutine 和 channel 简单、高效的解决并发问题,channel 解决的是 goroutine 之间的通信。
channel 是怎么设计的?
我们以为 channel 是一个通道:

实际上,channel 的内在是这样的:

channel 设计涉及的数据结构很简单:

基于数组的循环队列,有缓冲的 channel 用它暂存数据
基于链表的单向队列,用于保存阻塞在此 channel 上的 goroutine

我本来想自己码一篇 channel 的设计文章,但已经有大牛:Kavya 深入分析了 Channel 的设计,我也相信自己写的肯定不如他好,所以我把 Kavya 在 Gopher Con 上的 PPT 推荐给你,如果你希望成为 Go 大牛,你一定要读一下,现在请收藏好。
Kavya 在 Gopher Con 上的演讲主题是:理解 channel,他并不是教你如何使用 channel,而是把 channel 的设计和 goroutine 的调度结合起来,从内在方式向你介绍。这份 PPT 足足有 80 页,包含了大量的动画,非常容易理解,你会了解到:

channel 的创建
各种场景的发送和接收
goroutine 的调度
goroutine 的阻塞和唤醒
channel 和 goroutine 在 select 操作下

Kavya 的 PPT 应该包含了 channel 的 80% 的设计思想,但也有一些缺失,需要你阅读源码:

channel 关闭时,gorontine 的处理
创建 channel 时,不同的创建方法
读 channel 时的非阻塞操作

PPT 在此:Understanding Channels,如果你有心,还可以在这个网站看到 Kavya 关于 goroutine 调度的 PPT,福利哦????。(访问不了请翻墙,或阅读原文从博客文章最下面看 Github 备份)
channel 是怎么实现的?
chan.go 是 channel 的主要实现文件,只有 700 行,十分佩服 Go 团队,实现的如此精简,却发挥如此大的作用!!!
看完 Kavya 的 PPT,你已经可以直接看 channel 的源码了,如果有任何问题,思考一下你也可以想通,如果有任何问题可博客文章留言或公众号私信进行讨论。
另外,推荐一篇在 Medium(国外高质量文章社区)上获得 500+ 赞的源码分析文章,非常详细。
文章链接:Diving deep into the golang channels
我学到了什么?
阅读 channel 源码我学到了一些东西,分享给大家。
channel 的 4 个特性的实现:

channel 的 goroutine 安全,是通过 mutex 实现的。
channel 的 FIFO,是通过循环队列实现的。
channel 的通信:在 goroutine 间传递数据,是通过仅共享 hchan+ 数据拷贝实现的。
channel 的阻塞是通过 goroutine 自己挂起,唤醒 goroutine 是通过对方 goroutine 唤醒实现的。

channel 的其他实现:

发送 goroutine 是可以访问接收 goroutine 的内存空间的,接收 goroutine 也是可以直接访问发送 goroutine 的内存空间的,看 sendDirect、recvDirect 函数。
无缓冲的 channel 始终都是直接访问对方 goroutine 内存的方式,把手伸到别人的内存,把数据放到接收变量的内存,或者从发送 goroutine 的内存拷贝到自己内存。省掉了对方再加锁获取数据的过程。
接收 goroutine 读不到数据和发送 goroutine 无法写入数据时,是把自己挂起的,这就是 channel 的阻塞操作。阻塞的接收 goroutine 是由发送 goroutine 唤醒的,阻塞的发送 goroutine 是由接收 goroutine 唤醒的,看 gopark、goready 函数在 chan.go 中的调用。
接收 goroutine 当 channel 关闭时,读 channel 会得到 0 值,并不是 channel 保存了 0 值,而是它发现 channel 关闭了,把接收数据的变量的值设置为 0 值。
channel 的操作 / 调用,是通过 reflect 实现的,可以看 reflect 包的 makechan, chansend, chanrecv 函数。

如果阅读 chan_test.go 还会学到一些骚操作,比如:
if <-stopCh {
// do stop
}
而不是写成:
if stop := <-stopCh; stop {
// do stop
}
这就是关于 channel 的设计和实现的分享,希望你通过 Kavya 的 PPT 和代码阅读能深入了解 channel。
链接

chan.go:https://github.com/golang/go/…

chan_test.go:https://github.com/golang/go/…

Understanding channels 在 Github 的备份: https://github.com/Shitaibin/…

如果这篇文章对你有帮助,请点个赞 / 喜欢,感谢。
本文作者:大彬

如果喜欢本文,随意转载,但请保留此原文链接:http://www.lessisbetter.site/2019/03/03/golang-channel-design-and-source/

退出移动版