本篇描述了 有(无)缓冲通道的读写过程,并且说明了 使用通道的协程如何做到 先进先出,通道的缓冲数据如何做到先进先出。 在什么情况下 读写通道是阻塞的,什么情况下,读写通道是非阻塞的,最后介绍了通道的建议写法和应用场景。

一、读写通道的基本过程

1、无缓冲通道

无缓冲通道的结构如下:

“无缓冲区通道” 写过程:

1、若 接收队列 不为空,则从中取一个协程,假设为 A,把数据 memmove 到 A,唤醒A,返回true。

2、若 接收队列为空,且 是 非阻塞操作,则 返回 false。

3、若 接收队列为空,且 是 阻塞操作,则 把 curr_g 插入到 发送队列,阻塞 curr_g, 触发下一次调度。

“无缓冲区通道” 读过程:

1、若发送队列不为空,则从中取一个协程,假设为B, 把数据 memmove到B,唤醒B,返回true。

2、若 发送队列为空 , 且是非阻塞操作, 则返回 false。

3、若 发送队列为空, 且操作是阻塞的,则 把 curr_g 插入到 接收队列中,阻塞curr_g,让出cpu,触发下一次调度。

所以,无缓冲通道的读写过程很简单,先判断一下队列中有没有协程,有的话,直接 把数据 从一个 协程 memmove 到另一个协程就结束了。

如果队列中没有协程,要么返回false,要么把自己加到队列,阻塞自己,让出cpu,触发下一次调度。

2、有缓冲通道

有缓冲通道的结构如下:


“带缓冲区通道” 写过程:

1、若接收队列不为空(说明缓冲区肯定没有数据啦),则从接收队列 取 一个 协程,假设为C, 把数据 move 给 C,唤醒B,返回true。

2、若接收队列为空,

若缓冲区没满,则把数据写到缓冲区,返回true;

若缓冲区满了,且操作是非阻塞的,返回false;

若缓冲区满了,且操作是阻塞的,则把 curr_g 插入到 发送队列。阻塞 curr_g,让出cpu,触发下一次调度。

“带缓冲区通道”读过程:

1、若发送队列不为空(说明缓冲区已经满了),则从缓冲区读一个数据 --> 再从发送队列取一个 g,把g 的数据推入到缓冲区,并且唤醒这个g。 最后操作也是返回true。

2、若接收队列为空

若缓冲区有数据,则直接读数据,返回true

若缓冲区无数据,且操作是非阻塞的,返回false

若缓冲区无数据,且操作是阻塞的,则把 curr_g 插入到接收队列,阻塞自己,让出cpu,触发下一次调度。

二、一些细节

上面讲了 通道的读写大致过程。其中,有几点细节没说。

第一,什么情况下,操作是阻塞的,什么情况下,操作是非阻塞的。

第二,发送和接收队列,是一个什么结构,能保证协程 先进先出吗?(假设有两个协程 都阻塞在那里等待 读,能保证,先阻塞的那个 ,先获得数据吗)

第三,数据缓冲区是一个什么样的结构?能保证数据 先入先出吗?

首先,回答第一点。"什么情况下,操作是阻塞的,什么情况下,操作是非阻塞的"

在大部分情况下,读写通道都是阻塞的。

但是有一点例外,当 select + chan组合使用,且有default时,此时,操作chan 就是非阻塞的。(因为,如果chan没准备好,还有一个兜底的 default 可以执行)。

(by the way, 如果select {},且不含case和 false时,那么当前 g 会直接阻塞。且永远不会被唤醒,因为它没有唤醒条件哪,一般用在主协程 ,类似while(1))

有无defaultcase数量操作chan
1个阻塞
2个以上先 非阻塞式调用一遍所有的chan,若有就绪chan,则执行,并结束。
否则,把 当前g加到所有chan中,当g被唤醒时,再遍历一次,找到就绪的chan执行,并结束
1个及以上非阻塞调用chan,返回失败后,执行default,并结束。

第二点,“发送和接收队列,是一个什么结构,能保证协程 先进先出吗?”

答:发送和接收队列是一个双向的链表,它能保证协程 先进先出。

新增的协程放在链表的尾部,也就是 last指向的。当从队列中取协程时,则取 first指向的那个协程。也就是,发送和接收队列,能保证 先进先出。

第三点,“数据缓冲区是一个什么样的结构?能保证数据 先进先出吗?”

答:逻辑上,是一个环形缓冲区,它能保证数据先进先出。

缓冲区原本是一块连续的内存,这块连续的内存 再加上几个变量,逻辑上,就组成了一块环形缓冲区。

当 qcount < dataqize,表示缓冲区还没满。

当qcount = 0,表示缓冲区是空的。

最后还有一个细节,就是读写通道时,会有两种写法。这两种写法都是 读操作,建议使用第一种,后面会解释。

c1,ok := <-c

c1 := <-c

如果有N个协程阻塞在通道,此时,把通道关闭会怎么样。

runtime是这么做的:把通道的 recvq 或 sendq (两个不会同时存在哈),唤醒所有的g,那上面这个例子的ok 就是false了。

很多第三方的组件,利用chan 的这个特性,将协程优雅的推出。

一般的,主协程创建一个 stopchan,子协程 <-stopchan,若主协程想关闭所有的子协程,直接close(stopchan)就好了。

以上的总结,都是来自go的源码,Go\src\runtime\chan.go