channel的基本写操作
假如有一个元素类型为int的channel,变量名为ch,那么写操作(简称send为写)在代码中的写法如下所示:
ch <- 10
其中ch可能是“有缓冲”的,也可能是“无缓冲”的,甚至可能为nil。
按照上面的写法,有两种情况使写操作不会阻塞:
1)通道ch的“读等待队列”里已有goroutine在等待;
2)通道ch是“有缓冲”的,且缓冲区没有用尽。
第一种情况中,只要ch的读等待队列里有协程在排队,那么当前进行写请求的协程直接把数据交给队首的读协程就好了,无关ch有没有“缓冲”;
第二种情况中,ch是有缓冲的,且缓冲区没有用尽,也就是底层数组没有存满,那么当前执行写请求的协程直接把数据追加到缓冲数组中即可。
同样是上面的写法,有三种情况使写操作会阻塞:
1)通道ch为nil;
2)通道ch无缓冲且读等待队列为空;
3)通道ch有缓冲且缓冲区已用尽。
第一种情况中,参照golang的实现允许向nil通道执行写操作,但是会使当前写请求协程永远的阻塞在这个nil通道上,例如如下代码会因死锁抛出异常:
package main
func main() {
var ch chan int
ch <- 10
}
第二种情况中,ch为无缓冲通道,读等待队列中没有协程在等待,所以当前进行写请求的协程需要到通道的“写等待队列”中排队,当前写操作会阻塞;
第三种情况中,ch有缓冲且已用尽,隐含的信息就是读等待队列为空,否则缓冲不会用尽,所以当前进行写请求的协程只能到写等待队列中排队,当前写操作阻塞。
channel的非阻塞写操作
熟悉并发编程的同学应该知道,有些锁是支持tryLock操作的,也就是我想获得这把锁,但是万一已经被被人获得了,我不阻塞等待,可以继续去干其他事情。
在golang中,对于单个通道的非阻塞写操作可以用如下代码实现,注意是一个select、一个case和一个default,都是一个,不能多也不能少:
select {
case ch <- 10:
...
default:
...
}
如果检测到写ch不会阻塞,那么就会执行case ch <- 10:
分支,如果会阻塞,就会执行default:
分支。关于什么情况下会阻塞,什么情况下不会阻塞,参见上面的情况分析。
channel写操作的实现
上面简单的分析了channel的基本写操作和非阻塞写操作,虽然两者在形式上看起来有些差异,但是主要逻辑都是通过runtime.chansend函数实现的,下面简单的进行一下解读:
首先来看一下chansend函数的原型:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
c是一个hchan指针,指向要用来send数据的channel;
ep是一个指针,指向要被写入通道c的数据,数据类型和c的元素类型一致;
block表示如果写请求不能立即被满足,是否阻塞等待;
callerpc用以进行race相关检测,暂时不需要关心。
下面简单的过一遍chansend函数的主要逻辑:
1)在c为nil时,如果block为false,那么直接返回false,表示没有完成send数据的操作。如果block为true,那么就让当前协程”永久“的阻塞在这个nil通道上;
2)在block为false且closed为0,也就是”非阻塞“且通道”未关闭“的前提下,如果是”无缓冲“通道且读等待队列为空,或者是”有缓冲“通道且缓冲用尽,则直接返回false。有疑问的话,返回去看上面基本写操作的情况分析,本步判断是在不加锁的情况下进行的,为的是让非阻塞写在得不到满足时立即返回;
3)加锁,然后判断closed是否不为0,如果通道已经关闭,解锁,然后panic;
4)如果读等待队列不为空,从中取出第一个排队的协程,将数据传递给这个协程,并将其置为ready状态(进入run queue),然后解锁,返回true;
5)如果读等待队列为空,判断缓冲区是否还有剩余空间,在这里”无缓冲“自然被视为没有剩余空间。有剩余空间的话,将数据追加到缓冲区中,移动sendx,增加qcount,解锁,返回true;
6)如果缓冲区也已没有空间,判断block是否为false,如果是非阻塞写,解锁,返回false;
7)最后,到达这里就是阻塞写了,当前协程追加到通道的写等待队列中,等到写请求能够被满足时会被唤醒。
流程比较长,还是画个图吧:
本篇末尾做一下总结:
1)channel的基本写操作如c <- x
,会被编译器转换为对runtime.chansend1的调用,后者内部只是调用了runtime.chansend;
2)非阻塞式的写操作,即select、case、default三个一,会被编译器转换为对runtime.selectnbsend的调用,后者也仅仅是调用了runtime.chansend。非阻塞写的实现效果如下:
select {
case c <- v:
... foo
default:
... bar
}
// 被编译器转化为:
if selectnbsend(c, v) {
... foo
} else {
... bar
}
有疑问加站长微信联系(非本文作者)