首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

go语言核心—channel学习

这周来学习一下go语言的核心之一-channel,都知道go语言支持高并发,其原因就是goroutine-协程的存在,这是一种逻辑上等同于线程,而实际和线程又和线程有所不同,关于协程的学习,等下次结合这线程一起学习对比一下。今天主要简单的学习一下线程之间通讯的方式-channel。

一、channle的基本概念

channels是go中不同goroutines交互数据的一种通道,也就是说如果两个goroutine想要进行数据的传递,那么就必须使用channel。可以把channel理解成某种特定数据类型的管子,goroutine之间需要传递哪种类型的数据,那就是用哪种类型的channel。它的构造方法和map一样,使用的都是ch := make(chan int),这时候变量ch的类型是"chan int"类型。如果我们在函数中复制或者传递一个channel作为参数的时候,实际上传递的是它的引用。

关于channel的操作主要就是三种,分别是发送、接收和关闭。关闭使用的是close("channle"),而发送和接收都使用 "

ch

s =

还有一种就是只接收,但是不使用,或者说忽略

另外如果channel已经关闭,而依然向这个channel传递值会出现panic。

关于channle的构造方法ch := make(chan int)其实还有一个可选参数,代表了这个channel的容量,当然这里又引申出另外两个概念,缓冲channel和非缓冲channel。

先来说下非缓冲的channel,当向一个非缓冲channel发送数据的时候,当前的goroutine会进入阻塞状态,直到另一个goroutine在当前的channel上进行接收操作。反过来也是一样,如果一个接收操作先执行,那么当前的goroutine也会阻塞,直到另外一个goroutine在同一个channel上进行发送操作。从一定程度上可以认为非缓冲的channel是同步channel。

通过channel发送的message有两个重要的方面,每一个message都会有一个值,但是有时候使用channel传递message并不是为了获取它的值,而是获取它发生的时间点,这时候也可以称这个message为事件,即event。而当这个message没有额外值的时候,它的作用就仅仅是同步,或者说就是作为一个信号通知其他的goroutine。这时候channel传递message可以使用这样的形式:

因为书写方便,这种情况下可能使用channel传递一个int类型更常见。

二、Pipelines

channel用来不用的goroutine之间进行连接,也就是说这个goroutine的输出,是另一个goroutine的输入,这也可以称为管道(pipeline),看下下面这个例子:

上面这个列子我们建了两个channel以便在三个goroutine之间进行通讯,也就是主goroutine和两个go statement,第一个是创建4个整数,然后把值放到nums这个channel里面,然后由第二个channel接收,然后处理,再传递给squar。最后主goroutine接收并打印。但是打印完成后项目依然没有停止,因为主goroutine里面使用的无限的循环,就像java的 while(true)一样,而squar里面没有数据,这时候第二个goroutine阻塞了(是不是也可以理解为所有的goroutine都阻塞了呢?主goroutine再等第二个go,而第二个在等第一个go)。但是如果我知道我发送的数据量一定,比如上面的例子,我只发送4个数字,然后打印完后程序已经可以结束了。那么使用channel的close方法是不是就可以了呢?

我在第一个goroutine向nums发送完以后就关闭nums,即在for循环外加上close(nums)

然后再跑一遍程序,为了方便看效果修改下打印的循环条件,把无限循环定改成一个0-100的循环

打印的结果为:1 4 9 16 0 0 0 0 0 0 …..一直到循环结束。这个结果有点在意料之外,我以为打印到16以后,程序依然不会停止。

实际上,channel关闭以后,再对该channel进行操作会出现panic,但是最后一个数值被该channel下游的goroutine接收以后,下游的goroutine的接收操作不会阻塞,而是会被"0"值填充,如果打印那里依然是无限循环,那么打印完1 4 9 16后将会一直打印0 0 0 0….

对于一个channel有没有关闭并没有直接的方法去检验,但是对于接收操作有其实会有两个结果,一个是接收的数值,另一个是个布尔类型的值"ok",如果接收值成功那么ok的值就是true,否则就是false,可以通过"ok"值来判断nums是否关闭。上面的代码可以再修改一下,在第二个goroutine里面添加:

这时候再去执行代码就和想象中的一样了,即打印完 1 4 9 16后,程序依然没用结束,但是也没打印0值了。

go给提供了一种使用range来遍历channel的操作,这是一种更简便的操作,而且在接收完channel最后一个数值后就相应的goroutine就会终止。将代码再次进行修改:

再次启动程序以后再打印完1 4 9 16几个数字后,因为nums和squar都关闭,所以相应goroutine接收完数值后就关闭了,最后程序退出。不需要对每个channel都指向close()这个方法,只要保证channel上游的goroutine发送完数据后关闭即可,使用rang遍历出所有数值,并且接收不到新的数值后,当前的goroutine就会根据go的垃圾回收机制进行回收。

实际中根据项目的情况肯定会将复杂的业务进行抽取,上面的代码是全部写在main函数里面的明显是不合适的,所以这里需要将代码进行重构,根据业务分成三部分,一个是生成整数,另一个是对数字进行计算,最后是打印,代码如下:

为了防止channel的误用,go提供了一个所谓的“单向”channel类型,就比入上面的代码中nums就是发送的channel,而squar是一个接收的channel,他们都是只能完成一个操作,要不接收要不发送。关于发送和接收有时候容易迷,从代码里面看,nums接收了4个整数,似乎应该是一个接收的channel,为什么说是发送的channel呢?其实我觉得对于一个channel是接收还是发送要从goroutine之间的关系说起,我的理解是最上游的goroutine中的channel肯定是发送的,虽然它接收数值,但是它上游已经没用channel了,而channel是goroutine交互的通道。而最下游的goroutine的channel肯定是接收的,因为到它这里交互已经结束了。

未命名文件.jpg

channel创建的时候并不确定它是用来发送或者接收的,只是通过函数对它进行了一个相应的隐性转换,比如nums,转换成发送类型。但是对于squar这个似乎又有点疑问,因为它在中间,那么它是发送还是接收呢,或者说即是发送又是接收?答案是发送,channel是单向的,不可能即发送又接收,就好比水管一样,你水流只可能是单向流动的,只有在终结操作的goroutine前的channel才是接收类型,其余都是发送的。prints函数里面的squar就是一个接收类型(同一个channel不同的goroutine中作用是不同的)。对于square函数,它的参数nums是一个接收类型,而squar是发送类型。另外一点,close只能在发送类型的channel上操作。

三、缓冲channel

缓冲channel内部可以有一个包含相应元素的队列,上面说channel的构造方法时说道它有一个可选参数,这个可选参数就是它队列的大小。比如:ch = make(chan string,5),ch的类型是chan string,是一个缓冲的channel,可以容纳5个字符串。对于发送类型,是在队列后面插入数据;而接收类型则是在最前面取出数据。如果发送时队列已满,那么当前的goroutine就会进入阻塞状态,直到其他线程接收了它的数据,然后队列有新的空间。当然如果队列为空,接收操作也会阻塞(这个无论缓冲还是非缓冲都是一样的)。如果channel既不为空,也没满的话,可以进行发送也可以进行接收,并且都不会阻塞,这就是缓冲channel的优点吧。

如果想知道某个channel容量或者队列的大小,可以调用cap("channel")方法,而如果想知道当前channel中元素的个数可以调用len("channel")函数。cap、make、len都是go自带的内建函数。

下面这个方法创建了一个容量为20的字符串channel,并且用这个channel来接收了三个不同的goroutine的响应结果,但是,最终只会返回第一个,也就是说一个channel虽然可以接收来自不同的goroutine的数据,但是一旦它接收到了最先响应的数据,那么之后的其余两个goroutine的数据会被忽略。

func sendMsg() string {

msg := make(chan string,20)

go func() ()

go func() ()

go func() ()

return

}

但是如果goroutine想发送数据,而没有其他的goroutine来接收的时候那么它就会阻塞。上面的代码中就会出现这种情况,因为msg接收到最先响应的数据后,就会返回给主goroutine,这时候后面两个goroutine发送的数据不会被主goroutine接收,这样就会导致所谓的"goroutine leak",而这个泄漏的goroutine是不会被GC自动回收的,所以对于不再需要的goroutine一定要让它停止。

关于缓冲channel和非缓冲channel选择上,非缓冲channel能够提供更好的同步性能,因为它的发送和它下游goroutine的接收的操作是同步的,也就是说发送一个接收一个。而缓冲channel因为有一定的容量,即使下游goroutine没有接收,只要当前channel还未满依然可以继续发送,也就是说缓冲channel上下游goroutine的发送和接收可以不同步,二者之间关系是分离的。

在单个goroutine的程序中不应该使用channel,因为channel的作用是在不同的goroutine之间进行通讯的,所以如果把channel作为一个队列在单个goroutine中使用是不应该的,因为单个goroutine中永远不会出现其他的goroutine来接收它发送的数据,那么这个goroutine可能就会永久阻塞,channel虽好也不能乱用,一定要了解其使用的场景。多个goroutine可以同时通过一个channel发送或者接收数据。

channel作为go的核心内容之一,肯定不会这么简单,高并发情景下,channel也是影响性能的关键因素,但是自己缺少相应的经验,而且学习刚刚起步,只有在后面的学习过程中不断加强这方面的理解了。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181110G1NT7C00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券