作为Go最显著的特性之一,各种介绍channel的文章不胜枚举。今天不聊go的概念等基础内容,而是简单剖析一下channel基本原理,分享一些工作中的使用技巧。

1 channel的基本原理

在Go1.11的源码中channel是通过一个结构体来描述的,如下所示:

分析一下不难看出,一个channel主要由一个存放数据的环形队列,一组等待从环形队列中读取数据的而阻塞的goroutine和一组等待从环形队列中写取数据的而阻塞的goroutine,以及并发读写环形队列时的锁构成,这就是传统的生产者消费者模型。如果用new去创建一个chan就是生成一个上面的结构体的零值,没有任何意义。用make去创建chan时,不但会创建一个上面的结构体,同时也会初始化好背后所需要的各种资源(数据缓存,队列和锁等),并把这些资源同上述结构体中的各项对应关联起来,返回一个有血有肉的chan。当然,这些行为是在编译时由编译器确定的,make不过是个语法糖罢了。

2 channel可以用来干什么?

channel给人的直观感受就是用来在各个goroutine之间传输数据,这个和linux系统编程中的pipe、消息队列等在作用上没有什么太大区别。但真的只有这么简单吗?我们来看看channel还能干什么:

  • 实现Future/Promise模式

熟悉C++11的同学应该知道,在C++11中以标准库的方式引入了多线程,其中就定义了Future和Promise的概念。其基本原理形象点来说就是启动一个线程,并塞给线程一个盒子(Promise),线程把我想要的数据塞到这个盒子里(不一定就是返回值),我拿着这个盒子的钥匙(Future),在我想要取数据的时候就拿着钥匙去取。其背后的是具体实现就是通过两个shared_ptr共同持有一个内存块,存数据和放数据的双方在任务完成的时候都释放自己持有的share_ptr,该内存块自然也被释放。这里是C++11的知识,不多说了。

在Go中模拟C++11中Future/Promise方法就再简单不过了,启动goroutine的时候传一个chan进去,gotoutine把我想要的数据都发到这个chan上,我想要的时候去取就行了。或者,所有的goroutine都在这个chan上等我的命令,我想让你们干什么都会通过这个chan发给你们(比如,提前准备好一个goroutine池,池中的所有goroutine都在chan上待命)。

  • 实现锁和信号量机制

channel在写不进数据或者读不到数据的时候会阻塞,这就是一把天然的锁,很容易就能模拟Linux系统编程中的信号量和锁的功能。由于往channel中写数据实际上是将数据拷贝一份到channel的缓存中,这种情况下一般通过chan传递的数据是struct{}{},这样是零复制的,开销最小,只是为了达到通知的作用。比如,对一块可能并发写的数据我不想用mutex去保护,那么可以为这块数据配一个容量为1的chan。每次开始写之前先往这个chan中写一次数据,如果写成功则证明没人在访问临界数据,如果block则说明有人正在写临界数据。完成对临界数据的操作之后再读一次chan(相当于释放锁),其他因写而block的goroutine会有一个被唤醒,继续操作临界区。

  • 实现广播

close channel时会通知所有在该channel上等待读的goroutine唤醒,并收到一个数据类型的零值和ok=false的提示符,利用这一点可以给多个goroutine发一次广播。在实际的代码编写过程中,每次读channel成功后都需要同时接收ok信号,来判断是普通的发送数据(发送的零值数据)还是channel关闭引起的。

  • 实现多个goroutine之间动作的协调

通过一个channel来协调动作是很简单的,复杂情况下多通道协调动作就要使用select语句,这里不在赘述,用者自知。

channel的用途远不止上述几个,我总结的这几种情况都是最常用的。各种复杂复杂场景下都可以考虑看看能不能使用channel,有时channel的使用能给我们代码中的数据传输和动作协调带来异想不到的方便

3 如何正确地关闭 channel

现在大型软件的Go代码中到处都是channel,有时看的人眼花缭乱、云里雾里。channel一个很重要的特性就是向一个已关闭的channel写数据或是关闭一个已关闭的channel都会引起panic,造成程序崩溃。因此在select语句满天飞和关闭channel到处有的情况下,如何正确的关闭channel就是一个值得研究的问题了。一个基本的逻辑是:不要在接收端关闭channel,也不要关闭一个会被多个goroutine同时写的channel。换句话说,只有当一个goroutine是一个channel唯一的写入方时,其才能安全地关闭该channel。有人可能会想,如果channel都不关闭,岂不是资源泄漏了,因为channel还维护着数据缓存和读写两个队列。这样的人一定是C++出身的,天天担心资源的事(本人)。Go是自动gc的,对一个channel来说,当所有引用它的goroutine都退出的时候,这个channel就会在某个时刻被gc掉。换句话说,是否gc掉channel跟其是否被close是没有关系的,close只是channel的一个状态而已。

以上都是我在平时工作中学习到的一些心得,水平拙劣,欢迎读者在评论区不吝赐教。这是本人第一篇文章,希望后多写一些,和大家一起进步。文章会同步更新到下面的公众号,公众号也会不定期更新其他的技术内容,欢迎订阅。

本人的公众号