Golang channel 之 写操作 send

封幼麟 · · 1816 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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)最后,到达这里就是阻塞写了,当前协程追加到通道的写等待队列中,等到写请求能够被满足时会被唤醒。

流程比较长,还是画个图吧:


chansend.png

本篇末尾做一下总结:
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
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:封幼麟

查看原文:Golang channel 之 写操作 send

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1816 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传