通道(channel)介绍

通道是Go中的一种一等公民类型。它是Go的招牌特性之一。 和另一个招牌特性​​协程​​一起,这两个招牌特性使得使用Go进行并发编程(concurrent programming)变得十分方便和有趣,并且大大降低了并发编程的难度。

通道的主要作用是用来实现​​并发同步​​

Go提供了一种独特的并发同步技术来实现通过通讯来共享内存。此技术即为通道。 我们可以把一个通道看作是在一个程序内部的一个先进先出(FIFO:first in first out)数据队列。

一些协程可以向此通道发送数据,另外一些协程可以从此通道接收数据。

随着一个数据值的传递(发送和接收),一些数据值的所有权从一个协程转移到了另一个协程。 当一个协程发送一个值到一个通道,我们可以认为此协程释放了一些值的所有权。

当一个协程从一个通道接收到一个值,我们可以认为此协程获取了一些值的所有权。


通道类型和值

和数组、切片以及映射类型一样,每个通道类型也有一个元素类型。 一个通道只能传送它的(通道类型的)元素类型的值。

通道可以是双向的,也可以是单向的。

​chan T​​T​​chan<- T​​T​​<-chan T​​T​


每个通道值有一个容量属性。此属性的意义将在下一节中得到解释。 一个容量为0的通道值称为一个非缓冲通道(unbuffered channel),一个容量不为0的通道值称为一个缓冲通道(buffered channel)。


为了让解释简单清楚,在本文后续部分,通道将被归为三类:

  1. 零值(nil)通道;
  2. 非零值但已关闭的通道;
  3. 非零值并且尚未关闭的通道

下表简单地描述了三种通道操作施加到三类通道的结果。

操作

一个零值nil通道

一个非零值但已关闭的通道

一个非零值且尚未关闭的通道

关闭

产生恐慌

产生恐慌

成功关闭(C)

发送数据

永久阻塞

产生恐慌

阻塞或者成功发送(B)

接收数据

永久阻塞

永不阻塞(D)

阻塞或者成功接收(A)

对于上表中的五种未打上标的情形,规则很简单:

  • 关闭一个nil通道或者一个已经关闭的通道将产生一个恐慌。
  • 向一个已关闭的通道发送数据也将导致一个恐慌。
  • 向一个nil通道发送数据或者从一个nil通道接收数据将使当前协程永久阻塞。

下面将详细解释其它四种被打了上标(A/B/C/D)的情形。

为了更好地理解通道和为了后续讲解方便,先了解一下通道类型的大致内部实现是很有帮助的。

我们可以认为一个通道内部维护了三个队列(均可被视为先进先出队列):

  1. 接收数据协程队列(可以看做是先进先出队列但其实并不完全是,见下面解释)。此队列是一个没有长度限制的链表。 此队列中的协程均处于阻塞状态,它们正等待着从此通道接收数据。
  2. 发送数据协程队列(可以看做是先进先出队列但其实并不完全是,见下面解释)。此队列也是一个没有长度限制的链表。 此队列中的协程亦均处于阻塞状态,它们正等待着向此通道发送数据。 此队列中的每个协程将要发送的值(或者此值的指针,取决于具体编译器实现)和此协程一起存储在此队列中。
  3. 数据缓冲队列。这是一个循环队列(绝对先进先出),它的长度为此通道的容量。此队列中存放的值的类型都为此通道的元素类型。 如果此队列中当前存放的值的个数已经达到此通道的容量,则我们说此通道已经处于满槽状态。 如果此队列中当前存放的值的个数为零,则我们说此通道处于空槽状态。 对于一个非缓冲通道(容量为零),它总是同时处于满槽状态和空槽状态。

每个通道内部维护着一个互斥锁用来在各种通道操作中防止数据竞争。

​R​​R​
​R​​R​​R​​R​​R​
​S​​S​
​S​​S​​S​​S​​S​

上面已经提到过,一旦一个非零通道被关闭,继续向此通道发送数据将产生一个恐慌。 注意,向关闭的通道发送数据属于一个非阻塞操作

通道操作情形C: 当一个协程成功获取到一个非零且尚未关闭的通道的锁并且准备关闭此通道时,下面两步将依次执行:

  1. 如果此通道的接收数据协程队列不为空(这种情况下,缓冲队列必为空),此队列中的所有协程将被依个弹出,并且每个协程将接收到此通道的元素类型的一个零值,然后恢复至运行状态。
  2. 如果此通道的发送数据协程队列不为空,此队列中的所有协程将被依个弹出,并且每个协程中都将产生一个恐慌(因为向已关闭的通道发送数据)。 这就是我们在上面说并发地关闭一个通道和向此通道发送数据这种情形属于不良设计的原因。 事实上,并发地关闭一个通道和向此通道发送数据将产生数据竞争。

注意:当一个缓冲队列不为空的通道被关闭之后,它的缓冲队列不会被清空,其中的数据仍然可以被后续的数据接收操作所接收到。详见下面的对情形D的解释。

​true​​false​​false​
​select​
​select​

根据上面的解释,我们可以得出如下的关于一个通道的内部的三个队列的各种事实:

  • 如果一个通道已经关闭了,则它的发送数据协程队列和接收数据协程队列肯定都为空,但是它的缓冲队列可能不为空。
  • 在任何时刻,如果缓冲队列不为空,则接收数据协程队列必为空。
  • 在任何时刻,如果缓冲队列未满,则发送数据协程队列必为空。
  • 如果一个通道是缓冲的,则在任何时刻,它的发送数据协程队列和接收数据协程队列之一必为空。
  • 如果一个通道是非缓冲的,则在任何时刻,一般说来,它的发送数据协程队列和接收数据协程队列之一必为空, 但是有一个例外:一个协程可能在一个​​select流程控制​​中同时被推入到此通道的发送数据协程队列和接收数据协程队列中。

一些通道的使用例子

来看一些通道的使用例子来加深一下对上一节中的解释的理解。

一个简单的通过一个非缓冲通道实现的请求/响应的例子:


输出结果:


下面的例子使用了一个缓冲通道。此例子程序并非是一个并发程序,它只是为了展示缓冲通道的使用。


一场永不休场的足球比赛:


通道的元素值的传递都是复制过程

在一个值被从一个协程传递到另一个协程的过程中,此值将被复制至少一次。 如果此传递值曾经在某个通道的缓冲队列中停留过,则它在此传递过程中将被复制两次。 一次复制发生在从发送协程向缓冲队列推入此值的时候,另一个复制发生在接收协程从缓冲队列取出此值的时候。 和赋值以及函数调用传参一样,当一个值被传递时,​​只有它的直接部分被复制​​。

​65535​

关于通道和协程的垃圾回收

注意,一个通道被其发送数据协程队列和接收数据协程队列中的所有协程引用着。因此,如果一个通道的这两个队列只要有一个不为空,则此通道肯定不会被垃圾回收。 另一方面,如果一个协程处于一个通道的某个协程队列之中,则此协程也肯定不会被垃圾回收,即使此通道仅被此协程所引用。 事实上,一个协程只有在退出后才能被垃圾回收。

数据接收和发送操作都属于简单语句

数据接收和发送操作都属于​​简单语句​​​。 另外一个数据接收操作总是可以被用做一个单值表达式。 简单语句和表达式可以被用在​​一些控制流程​​的某些部分。

​for​


​for-range​
​for-range​​for-range​​for-range​

等价于


​aChannel​​for-range​
​for​


​select-case​
​select-case​​switch-case​​select-case​​case​​default​​select-case​
​select​​{​​fallthrough​​case​​case​​case​​case​​case​​case​​default​​default​
​select-case​​select{}​
​default​​case​
​case​​default​​select-case​
​select-case​​case​​case​
​select-case​
​select-case​​select-case​
​case​​default​​case​​case​​N​​N​​case​​N​
​case​​case​​case​​default​​default​​default​​case​
​case​​case​​case​​select-case​​case​​case​​case​
​case​​case​
  1. 完毕。

从此实现中,我们得知

​select-case​

相关文献:golang 并发编程 通道用例大全