A. 通道简介
- 通道是 Go 的一等公民类型,主要用于实现并发同步。
- 通道支持通过通信共享内存,而非通过共享内存进行通信。
- 通道可以看作是一个先进先出(FIFO)的数据队列,协程可以通过通道发送和接收数据。
B. 通道类型和值
- 通道分为双向和单向,单向通道只能发送或接收数据。
- 通道有容量属性,容量为零的通道是非缓冲的,否则为缓冲通道。
C. 通道操作
- Go 中有五种通道相关的操作:关闭通道、发送数据、接收数据、查询通道容量、查询通道长度。
- 这些操作是并发安全的,但通道赋值和从通道接收后的值赋值是非同步的。
操作 | 一个零值nil通道 | 一个非零值但已关闭的通道 | 一个非零值且尚未关闭的通道 |
---|---|---|---|
关闭 | 产生恐慌 | 产生恐慌 | 成功关闭(C) |
发送数据 | 永久阻塞 | 产生恐慌 | 阻塞或者成功发送(B) |
接收数据 | 永久阻塞 | 永不阻塞(D) | 阻塞或者成功接收(A) |
C.1. 零值 nil 通道为什么关闭会恐慌?
一个 nil
通道并没有实际的通道资源分配给它,因此对其进行任何操作(如关闭、发送或接收)都没有意义,会导致异常行为。
D. 详细原因
- 通道未初始化:
nil
通道意味着通道变量还没有被初始化,实际上它并不指向任何有效的通道结构。 - 资源不可用:由于
nil
通道不指向任何有效的通道,试图关闭它就像试图对不存在的对象进行操作,Go 运行时无法执行该操作,因为没有实际的通道资源可以关闭。 - 设计意图:Go 语言设计中,
nil
通道通常用来表示通道尚未分配,或者用作一种信号,告诉程序当前通道不可用。为了避免误操作,任何对nil
通道的关闭操作设计为直接导致恐慌,以便程序员在开发阶段就能发现和修复此类错误。
E. 其他通道操作
- 发送数据:对
nil
通道的发送操作会使当前协程永久阻塞,因为没有地方可以存储数据。 - 接收数据:对
nil
通道的接收操作同样会使当前协程永久阻塞,因为没有数据可以接收。v
E.1. 非零值但已关闭的通道为什么接受数据会永不阻塞
一个非零值但已关闭的通道在接收数据时永不阻塞,这是因为通道关闭后,接收操作会立即返回通道元素类型的零值,而不会等待新的数据到来。
E.1.1. 详细原因
- 通道关闭状态:当通道被关闭时,意味着不会再有新的数据被发送到该通道。因此,接收操作不需要等待数据的到来。
- 返回零值:对于一个已关闭的通道,接收操作会立即返回该通道元素类型的零值,并且第二个返回值(如果有)会是
false
,表示通道已关闭。 - 设计意图:这种设计使得程序可以通过检查接收操作的第二个返回值来判断通道是否已关闭,从而决定是否继续处理数据或终止操作。
这种机制确保了程序在处理已关闭通道时的稳定性和可预测性,避免了因等待数据而导致的阻塞。
E.2. 通道内部实现
- 通道维护三个队列:接收协程队列、发送协程队列和数据缓冲队列。队列的状态决定了操作的阻塞与否。
- 接收协程队列:存放等待接收数据的协程。
- 发送协程队列:存放等待发送数据的协程。
- 数据缓冲队列:循环队列,长度为通道的容量,存放通道元素。
E.3. 通道操作详解+四种情况
- 接收操作(情形A):
- 如果缓冲队列非空,直接接收。
- 如果发送队列非空,从发送队列接收。
- 否则,协程阻塞等待。
- 发送操作(情形B):
- 如果接收队列非空,直接发送。
- 如果缓冲队列未满,数据入缓冲队列。
- 否则,协程阻塞等待。
- 关闭操作(情形C):
- 接收队列中的协程接收零值。
- 发送队列中的协程产生恐慌。
- 接收操作(情形D):
- 关闭后接收操作不阻塞,缓冲数据仍可接收,直到缓冲队列为空。
E.3.1. 一个协程 R
尝试从一个非零且尚未关闭的通道接收数据
|
|
通道操作情形A: 当一个协程
R
尝试从一个非零且尚未关闭的通道接收数据的时候,此协程R
将首先尝试获取此通道的锁,成功之后将执行下列步骤,直到其中一个步骤的条件得到满足。
- 如果此通道的缓冲队列不为空(这种情况下,接收数据协程队列必为空),此协程
R
将从缓冲队列取出(接收)一个值。 如果发送数据协程队列不为空,一个发送协程将从此队列中弹出,此协程欲发送的值将被推入缓冲队列。此发送协程将恢复至运行状态。 接收数据协程R
继续运行,不会阻塞。对于这种情况,此数据接收操作为一个非阻塞操作。- 否则(即此通道的缓冲队列为空),如果发送数据协程队列不为空(这种情况下,此通道必为一个非缓冲通道), 一个发送数据协程将从此队列中弹出,此协程欲发送的值将被接收数据协程
R
接收。此发送协程将恢复至运行状态。 接收数据协程R
继续运行,不会阻塞。对于这种情况,此数据接收操作为一个非阻塞操作。- 对于剩下的情况(即此通道的缓冲队列和发送数据协程队列均为空),此接收数据协程
R
将被推入接收数据协程队列,并进入阻塞状态。 它以后可能会被另一个发送数据协程唤醒而恢复运行。 对于这种情况,此数据接收操作为一个阻塞操作。
E.3.2. 一个协程 S
尝试向一个非零且尚未关闭的通道发送数据
|
|
通道操作情形B: 当一个协程
S
尝试向一个非零且尚未关闭的通道发送数据的时候,此协程S
将首先尝试获取此通道的锁,成功之后将执行下列步骤,直到其中一个步骤的条件得到满足。
- 如果此通道的接收数据协程队列不为空(这种情况下,缓冲队列必为空), 一个接收数据协程将从此队列中弹出,此协程将接收到发送协程
S
发送的值。此接收协程将恢复至运行状态。 发送数据协程S
继续运行,不会阻塞。对于这种情况,此数据发送操作为一个非阻塞操作。- 否则(接收数据协程队列为空),如果缓冲队列未满(这种情况下,发送数据协程队列必为空), 发送协程
S
欲发送的值将被推入缓冲队列,发送数据协程S
继续运行,不会阻塞。 对于这种情况,此数据发送操作为一个非阻塞操作。- 对于剩下的情况(接收数据协程队列为空,并且缓冲队列已满),此发送协程
S
将被推入发送数据协程队列,并进入阻塞状态。 它以后可能会被另一个接收数据协程唤醒而恢复运行。 对于这种情况,此数据发送操作为一个阻塞操作。上面已经提到过,一旦一个非零通道被关闭,继续向此通道发送数据将产生一个恐慌。 注意,向关闭的通道发送数据属于一个非阻塞操作。
E.3.3. 一个协程成功获取到一个非零且尚未关闭的通道的锁并且准备关闭此通道时
|
|
通道操作情形C: 当一个协程成功获取到一个非零且尚未关闭的通道的锁并且准备关闭此通道时,下面两步将依次执行:
- 如果此通道的接收数据协程队列不为空(这种情况下,缓冲队列必为空),此队列中的所有协程将被依个弹出,并且每个协程将接收到此通道的元素类型的一个零值,然后恢复至运行状态。
- 如果此通道的发送数据协程队列不为空,此队列中的所有协程将被依个弹出,并且每个协程中都将产生一个恐慌(因为向已关闭的通道发送数据)。 这就是我们在上面说并发地关闭一个通道和向此通道发送数据这种情形属于不良设计的原因。 事实上,在数据竞争侦测编译选项(
-race
)打开时,Go官方标准运行时将很可能会对并发地关闭一个通道和向此通道发送数据这种情形报告成数据竞争。 注意:当一个缓冲队列不为空的通道被关闭之后,它的缓冲队列不会被清空,其中的数据仍然可以被后续的数据接收操作所接收到。详见下面的对情形D的解释。
E.3.4. 一个非零通道被关闭之后,此通道上的后续数据接收操作将永不会阻塞
|
|
通道操作情形 D: 一个非零通道被关闭之后,此通道上的后续数据接收操作将永不会阻塞。此通道的缓冲队列中存储数据仍然可以被接收出来。伴随着这些接收出来的缓冲数据的第二个可选返回(类型不确定布尔)值仍然是 true。一旦此缓冲队列变为空,后续的数据接收操作将永不阻塞并且总会返回此通道的元素类型的零值和值为 false 的第二个可选返回结果。上面已经提到了,一个接收操作的第二个可选返回(类型不确定布尔)结果表示一个接收到的值是否是在此通道被关闭之前发送的。如果此返回值为 false,则第一个返回值必然是一个此通道的元素类型的零值。
F. 通道的内部实现
- 通道维护三个队列:接收协程队列、发送协程队列和数据缓冲队列。
- 各种通道操作的阻塞与否取决于这些队列的状态。
G. 通道的特性和使用
- 通过例子展示了非缓冲通道和缓冲通道的用法。
- 提到了通道元素值的传递是复制过程,并介绍了垃圾回收对通道和协程的影响。
H. select-case
语句
select-case
是专为通道设计的流程控制结构,类似于switch-case
,但有其独特之处。select-case
语句的实现步骤被详细列出。