本篇描述了 有(无)缓冲通道的读写过程,并且说明了 使用通道的协程如何做到 先进先出,通道的缓冲数据如何做到先进先出。 在什么情况下 读写通道是阻塞的,什么情况下,读写通道是非阻塞的,最后介绍了通道的建议写法和应用场景。
一、读写通道的基本过程
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))
有无default | case数量 | 操作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