第212章 直接提交测试

书名:重生之我是卷王 作者:The 本章字数:3350 下载APP
道(channel)介绍:Go语言设计团队的首任负责人Rob Pike对并发编程的一个建议 是不要让计算通过共享内存来通讯,而应该让它们通过通讯来共享内存。  通道机制就是这种哲学的一个设计结果。(在Go编程中,我们可以认为一个计算就是一个协程。)
通过共享内存来通讯 和 通过通讯来共享内存 是并发编程中的两种编程风格。
当通过共享内存来通讯的时候,需要一些传统的并发同步技术(比如互斥锁)来避免数据竞争。
Go提供了一种独特的并发同步技术来实现通过通讯来共享内存。 此技术即为通道。 我们可以把一个通道看作是在一个程序内部的一个先进先出(FIFO:first in first out)数据队列。
一些协程可以向此通道发送数据,另外一些协程可以从此通道接收数据。
随着一个数据值的传递(发送和接收),一些数据值的所有权从一个协程转移到了另一个协程。
当一个协程发送一个值到一个通道,我们可以认为此协程释放了(通过此发送值可以访问到的)一些值的所有权。
当一个协程从一个通道接收到一个值,我们可以认为此协程获取了(通过此接受值可以访问到的)一些值的所有权。
当然,在通过通道传递数据的时候,也可能没有任何所有权发生转移。
通道类型和值
和数组、切片以及映射类型一样,每个通道类型也有一个元素类型。 一个通道只能传送它的(通道类型的)元素类型的值。
通道可以是双向的,也可以是单向的。
字面形式chan T : 表示一个元素类型为T的双向通道类型。 编译器允许从此类型的值中接收和向此类型的值中发送数据。
字面形式chan<- T : 表示一个元素类型为T的单向发送通道类型。 编译器不允许从此类型的值中接收数据。
字面形式<-chan T : 表示一个元素类型为T的单向接收通道类型。 编译器不允许向此类型的值中发送数据。
双向通道chan T的值可以被隐式转换为单向通道类型chan<- T 和<-chan T ,但反之不行(即使显式也不行)。 类型chan<- T和<-chan T的值也不能相互转换。
每个通道值有一个容量属性。此属性的意义将在下一节中得到解释。 一个容量为0的通道值称为一个非缓冲通道(unbuffered channel),一个容量不为0的通道值称为一个缓冲通道(buffered channel)。
通道类型的零值也使用预声明的nil来表示。 一个非零通道值必须通过内置的make函数来创建。
比如make(chan int, 10)将创建一个元素类型为int的通道值。 第二个参数指定了欲创建的通道的容量。此第二个实参是可选的,它的默认值为0。
通道值的比较
所有通道类型均为可比较类型。
当一个通道值被赋给另一个通道值后,这两个通道值将共享相同的底层部分。
换句话说,这两个通道引用着同一个底层的内部通道对象。 比较这两个通道的结果为true。
通道操作
Go中有五种通道相关的操作。假设一个通道(值)为ch,下面列出了这五种操作的语法或者函数调用。


调用内置函数close来关闭一个通道:
close(ch): 传给close函数调用的实参必须为一个通道值,并且此通道值不能为单向接收的。


使用下面的语法向通道ch发送一个值v:
ch <- v: v必须能够赋值给通道ch的元素类型。 ch不能为单向接收通道。 <-称为数据发送操作符。


使用下面的语法从通道ch接收一个值:
<-ch: 如果一个通道操作不永久阻塞,它总会返回至少一个值,此值的类型为通道ch的元素类型。
ch不能为单向发送通道。 <-称为数据接收操作符,是的它和数据发送操作符的表示形式是一样的。


在大多数场合下,一个数据接收操作可以被认为是一个单值表达式。
但是,当一个数据接收操作被用做一个赋值语句中的唯一的源值的时候,它可以返回第二个可选的类型不确定的布尔值返回值从而成为一个多值表达式。
此类型不确定的布尔值表示第一个返回值是否是在通道被关闭之前被发送的。(从后面的章节,我们将得知我们可以从一个已关闭的通道中接收到无穷个值。)
数据接收操作在赋值中被用做源值的例子:
ini 代码解读复制代码v = <-ch
v, sentBeforeClosed = <-ch



查询一个通道的容量: cap(ch)
其中cap是一个已经在容器类型一文中介绍过的内置函数。 cap的返回值的类型为内置类型int。


查询一个通道的长度: len(ch)
len的返回值的类型也为内置类型int。 一个通道的长度是指当前有多少个已被发送到此通道但还未被接收出去的元素值。


上面列出来的五种操作都是同步的,线程安全的。
通道操作详解
为了让解释简单清楚,在本文后续部分,通道将被归为三类:

零值(nil)通道;
非零值但已关闭的通道;
非零值并且尚未关闭的通道。
下表简单地描述了三种通道操作施加到三类通道的结果。






























操作一个零值nil通道一个非零值但已关闭的通道一个非零值且尚未关闭的通道关闭产生恐慌产生恐慌成功关闭(C)发送数据永久阻塞产生恐慌阻塞或者成功发送 (B)接收数据永久阻塞永不阻塞(D)阻塞或者成功接收(A)
对于上表中的五种未打上标的情形,规则很简单:

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

下面将详细解释其它四种被打了上标(A/B/C/D)的情形。
为了更好地理解通道和为了后续讲解方便,先了解一下通道类型的大致内部实现是很有帮助的。
可以认为一个通道内部维护了三个队列(均可被视为先进先出队列):


接收数据协程队列(可以看做是先进先出队列但其实并不完全是,见下面解释)。此队列是一个没有长度限制的链表。 此队列中的协程均处于阻塞状态,它们正等待着从此通道接收数据。


发送数据协程队列(可以看做是先进先出队列但其实并不完全是,见下面解释)。此队列也是一个没有长度限制的链表。 此队列中的协程亦均处于阻塞状态,它们正等待着向此通道发送数据。 此队列中的每个协程将要发送的值,和此协程一起存储在此队列中。


数据缓冲队列。这是一个循环队列(绝对先进先出),它的长度为此通道的容量。此队列中存放的值的类型都为此通道的元素类型。 如果此队列中当前存放的值的个数已经达到此通道的容量,则我们说此通道已经处于满槽状态。 如果此队列中当前存放的值的个数为零,则我们说此通道处于空槽状态。 对于一个非缓冲通道(容量为零),它总是同时处于满槽状态和空槽状态。


小结:通道是内部安全的,内部维护了三个队列,进数据的G,取数据的G,以及内部存放数据的缓冲队列。相当于控制权都交给了channel。
每个通道内部维护着一个互斥锁用来在各种通道操作中防止数据竞争。
通道操作情形A: 当一个协程R尝试从一个非零且尚未关闭的通道接收数据的时候,此协程R将首先尝试获取此通道的锁,成功之后将执行下列步骤,直到其中一个步骤的条件得到满足。


如果此通道的缓冲队列不为空(这种情况下,接收数据协程队列必为空),此协程R将从缓冲队列取出(接收)一个值。 如果发送数据协程队列不为空,一个发送协程将从此队列中弹出,此协程欲发送的值将被推入缓冲队列。此发送协程将恢复至运行状态。 接收数据协程R继续运行,不会阻塞。对于这种情况,此数据接收操作为一个非阻塞操作。


否则(即此通道的缓冲队列为空),如果发送数据协程队列不为空(这种情况下,此通道必为一个非缓冲通道), 一个发送数据协程将从此队列中弹出,此协程欲发送的值将被接收数据协程R接收。此发送协程将恢复至运行状态。 接收数据协程R继续运行,不会阻塞。对于这种情况,此数据接收操作为一个非阻塞操作。


对于剩下的情况(即此通道的缓冲队列和发送数据协程队列均为空),此接收数据协程R将被推入接收数据协程队列,并进入阻塞状态。 它以后可能会被另一个发送数据协程唤醒而恢复运行。 对于这种情况,此数据接收操作为一个阻塞操作。