上一篇:Golang channel 之 数据结构
下一篇:Golang channel 之 读操作 recv
channel的常规写操作
假如有一个元素类型为int的channel,变量名为ch,那么常规的写操作(简称send为写)在代码中的写法如下所示:
ch <- 10
其中ch可能是“有缓冲”的,也可能是“无缓冲”的,甚至可能为nil。
按照上面的写法,有两种情况能使写操作不会阻塞:
1)通道ch的recvq里已有goroutine在等待;
2)通道ch是“有缓冲”的,且缓冲区没有用尽。
第一种情况中,只要ch的recvq里有协程在排队,当前协程就直接把数据交给recvq队首的那个协程就好了,然后两个协程都可以继续执行,无关ch有没有缓冲;
第二种情况中,ch是有缓冲的,且缓冲区没有用尽,也就是底层数组没有存满,那么当前协程直接把数据追加到缓冲数组中,就可以继续执行。
同样是上面的写法,有三种情况会使写操作阻塞:
1)通道ch为nil;
2)通道ch无缓冲且recvq为空;
3)通道ch有缓冲且缓冲区已用尽。
第一种情况中,参照golang的实现,允许对nil通道执行写操作,但是会使当前协程永久性的阻塞在这个nil通道上,例如如下代码会因死锁抛出异常:
package main
func main() {
var ch chan int
ch <- 10
}
第二种情况中,ch为无缓冲通道,recvq中没有协程在等待,所以当前协程需要到通道的sendq中排队;
第三种情况中,ch有缓冲且已用尽,隐含的信息就是recvq为空,否则缓冲不会用尽,所以当前协程只能到sendq中排队。
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相关检测,暂时不需要关心;
返回值为true表示数据写入完成,false表示目前不可写但因为不想阻塞(block为false)而返回。
chansend函数的主要逻辑如下:
如果c等于nil {
如果不想阻塞 {
return false
}
永久阻塞
}
如果(不想阻塞 且 c未关闭) 且 ((c无缓冲 且 recvq是空的) 或 (c有缓冲 且 缓冲区已满)) {
return false
}
对c加锁
如果c已关闭 {
解锁c
panic("send on closed channel")
}
如果recvq中有内容 {
取出队首协程,并把数据传递给它并解锁c
return true
}
如果缓冲区还有空间 {
把数据追加到缓冲区,移动sendx,递增qcount
解锁c
return true
}
如果不想阻塞 {
解锁c
return false
}
进入sendq排队等待同时解锁c,条件满足时会完成数据传递并被唤醒
return true
逐块对应以上代码:
1)如果c为nil,进一步判断block:如果block为false,那么直接返回false,表示未send数据;如果block为true,那么就让当前协程”永久“的阻塞在这个nil通道上;
2)如果block为false且closed为0,也就是在”不想阻塞“且通道”未关闭“的前提下,如果是通道”无缓冲“且recvq为空,或者是通道”有缓冲“且缓冲已用尽,则直接返回false。本步判断是在不加锁的情况下进行的,目的是让非阻塞写在无法立即完成时能真正不阻塞(加锁可能阻塞)。此处有疑问的话,可返回上面看常规写操作的情况分析;
3)加锁;
4)如果closed不为0,即通道已经关闭的话,则解锁,然后panic。因为不可以写入已关闭的通道;
5)如果recvq不为空,就从中取出第一个排队的协程,将数据传递给这个协程,并将该协程置为ready状态(放入run queue,进而得到调度),然后解锁,返回true;
6)通过比较qcount和dataqsiz判断缓冲区是否还有剩余空间,在这里”无缓冲“的通道被视为没有剩余空间。有剩余空间的话,将数据追加到缓冲区中,移动sendx,增加qcount,解锁,返回true;
7)如果block为false,即不想阻塞,则解锁,返回false;
8)最后,到达这里就是阻塞写了,当前协程把自己追加到通道的sendq中阻塞排队,同时解锁,等到条件满足时会被唤醒。
流程比较长,还是画个图吧:
本篇总结
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
}