教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 【文末有惊喜!】一文读懂golang channel

【文末有惊喜!】一文读懂golang channel

发布时间:2021-12-05   编辑:jiaochengji.com
教程集为您提供【文末有惊喜!】一文读懂golang channel等资源,欢迎您收藏本站,我们将为您提供最新的【文末有惊喜!】一文读懂golang channel资源

  

本文字数:4042

预计阅读时间:15分钟

前言

golang是一门非常适合开发高并发服务的语言,而goroutine是golang并行设计的核心。goruntine其实是语言级协程,它非常的轻量,只需要非常少的内存(4~5kb,当然会根据运行环境和数据进行伸缩)。也正因为它的轻量,使得golang的开发者能够通过简单的代码就能够搭起能支撑成千上万并发的web服务。而在golang并发编程中最核心的问题,就是如何进行goroutine间通信。

golang社区中有一句非常经典的话:不要通过共享内存来通信,而应该通过通信来共享内存。在并发场景下,通过共享内存进行进程间通信,需要解决数据的线程安全问题,要加大量的锁。当然在golang所提供的sync包中也提供了传统的锁机制,但程序中使用过多的锁,就会加大代码的复杂度,拉低web服务性能,还稍有不慎就会造成死锁,程序崩溃等等。而golang提供的channel机制,为开发者提供了一种优雅的方式,解决goroutine间通信问题。

channel的作用

channel可以理解为一块单独的内存,根据声明的channel类型不同,可以放1个或以上的数据值。如果channel中的数据已满,那数据的生产者的发送操作会被阻塞直到channel中的数据值被消费者取走。如果channel里没有数据时,那么消费者的读取操作会被阻塞直到生产者往channel中放入新的数据为止。

channel分类

channel 分两种:Unbuffered channels(无缓冲channel,也称为同步channel) 和 Buffered Channels (缓冲形channel)

Unbuffered channels

<ol><li>

unbuffered channels用于多goroutine之间同步通信

</li><li>

必须使用make来创建channel,并指定channel中存放数据的类型

</li><li>

通过操作符 <- 来发送或读取channel中的数据

</li></ol><pre class="has"><code class="language-go">c1 := make(chan int) //用于放置int类型数据的channel c2 := make(chan string) //用于放置string类型数据的channel c1 <- 1 //发送数据到int类型的channel ch := <-c1 //读取channel中的数据保存到变量ch中 </code></pre>

Unbuffered channels示例

了解基本语法后,我们来看一个channel的应用例子:

<pre class="has"><code class="language-go">package main import "fmt" func main() {     ch := make(chan int)     ch <- 1     fmt.Println(<-ch) } </code></pre>

上面的代码示例非常简单,创建一个存放int类型数据的channel,往channel中放入数据后,将channel读出并打印。看似完全符合语法逻辑,可当我们编译执行该go文件后,却得到了下面的报错:

这是unbuffered channels应用中一个最经典的死锁。这个死锁是怎么造成的?要了解该死锁的原因,先了解操作unbuffered channels 发送/读取数据的两个原则:

<ol><li>

数据的生产者必须在消费者准备就绪的情况下,才能够往channel中发送数据。channel中有数据时,写操作阻塞。

</li><li>

数据的消费者必须在生产者准备就绪且channel中有数据的情况下才能够读取数据。channel中无数据时,读操作阻塞。

</li></ol>

而上图中的示例之所以会发生死锁,是因为生产者(第7行)往unbuffered channel中发送数据前,必须等待消费者(第8行)准备就绪。当程序执行到第7行时,由于消费者未就绪,生产者发送操作被阻塞。同时又因为main线程阻塞,无法执行到第8行,导致消费者永远无法就绪,相互等待导致死锁。要想解决死锁,必须将消息的发送和接收操作放在不同的goroutine中(也可以把其中一个放到main进程中),如下图的示例:

笔者建议初学者在刚进行channel开发时,可以在生产者/消费者的声明中指定对channel的操作方向,如上图中的"c <em><-chan</em> string" 和 "c <em>chan<-</em> string"。"<-chan" 用于声明该变量只能用于读取,"chan<-"用于声明该变量只能用于写入。这样的强制声明有助于在开发复杂的程序逻辑时避免弄错操作方向,不慎造成死锁。如下方的示例:

Buffered Channels

1.buffered channel用于goroutine间的异步通信

2.必须使用make创建channel,同时指定channel中存放的数据类型和缓冲区大小

<pre class="has"><code class="language-go">c1 := make(chan string, 10) //创建一个缓冲区大小为10的缓冲channel c1 <- "hello" //发送数据到string类型的缓冲channel ch := <-c1    //读取channel中的数据保存到变量ch中 </code></pre>

Buffered Channels示例

在上图示例中,我们创建了一个缓冲区长度为1的缓冲channel,然后往缓冲channel放入数据,最后读取出来并打印。这里我们可以看到Unbuffered channel和Buffered channel之间最大的区别。相似的代码逻辑,使用Buffered channel却不会造成死锁。也就是说即使Buffered channel声明的缓冲区长度为1(只能一次发送一个数据),但也不等同于Unbuffered channel。下面是Buffered channel的两个原则:

1.生产者在往Buffered channel放入数据时,不需要等待消费者就绪,只要channel缓冲区有位置即可放入。缓冲区满时写操作阻塞。

2.消费者读取Buffered channel中的数据时,不需要等待生产者就绪,只要channel缓冲区中有数据即可读取。缓冲区空时读操作阻塞。

为了更直观的对比Unbuffered channel和Buffered channel,笔者做了下面的对比:

<table><thead><tr><th>
</th><th>Unbuffered channel</th><th>Buffered channel</th></tr></thead><tbody><tr><td>相同点</td><td>1.有数据时阻塞写操作
2.无数据时阻塞读操作</td><td>1.缓冲区满时阻塞写操作
2.缓冲区无数据时阻塞读操作</td></tr><tr><td>不同点</td><td>1.只能放一个变量
2.同步通信,通信双方必须都为就绪状态</td><td>1.可以放一个及以上变量
2.异步通信,通信时双方无需都为就绪状态</td></tr></tbody></table>

引用The Nature Of Channels In Go中的两张图来说明Unbuffered channels 和 Buffered channel:

Unbuffered channel:

buffered channels:

总结

1.使用Unbuffered channel通信就像是<em>两个人在打电话</em>,通话的双方都必须是准备就绪的状态,不能干别的事情,通话的内容也是实时交换的。

2.使用Buffered channel通信就像是两个人共用<em>一个信箱通过写信来传递消息</em>,写信的人不用关心收信人的状态,收信人收信的时候也不用关心写信人的状态。只要信箱装得下,写信人就可以一直塞信进去。同样只要信箱不空,收信人就可以一直收取新的信件。

对比完两种channel的不同点,我们再来讲讲channel还有哪些操作和特性:

1.两种channel都必须用make实例化。对于为nil的channel,读/写操作都阻塞。close一个nil的channel会导致panic。2.两种channel都可以重复使用,不断的读/写数据。开发过程中,可以使用普通的for循环或更灵活的range方式读取数据。使用range方式,能够不断的读取channel中数据,直到channel被显式的关闭。

3.channel可以通过chose来关闭。但要注意的是:

<ul><li>

已经被关闭的channel,生产者是无法再写入数据的(会导致panic),但关闭的channel依然可以被消费者读取。消费者可以通过 data,ok:=<-ch 的方式来获取通道中的数据和channel是否已被关闭(当ok==false时为关闭)。

</li><li>

当只有一个生产者时,channel应该有生产者发送完数据后或需要结束消费者的range操作时关闭。

</li><li>

有多个生产者时,如果生产者明确知道消息已经发送完且channel未被关闭的情况下,建议由生产者关闭channel。或者可以通过引入监控者监控生产者状态,最后负责关闭channel。close一个已经被关闭的生产者会导致panic,虽然可以使用recover panic的方式保证程序正常运行,但如此粗暴的处理方式,有强迫症的笔者表示可以用但不太建议。

</li><li>

close channel不是必须的,如果channel没有再被goroutine引用,最终也会被GC清理掉

</li></ul>

4.可以通过select来管理多个channel的读/写操作。当有多个channel准备就绪时,会随机选择其中一个case操作执行。select类似平常用的switch,不同的case可用于监听不同channel的写入/读取操作,而当所有监听的channel都未准备就绪时,会默认执行default操作。

如上图,main线程监听ch的读取操作,由于子goroutine执行time.Sleep 1s操作,导致ch的生产者无法就绪,ch的读操作阻塞,main线程默认执行了default中定义的操作。如果想要保持持续监听的状态,可以用for{}将该select代码块包起来。

channel的典型应用

1.并发处理多任务,并收集处理执行结果

如上图示例所示,需要根据n个id查询n个用户的信息,并以最快的速度返回结果。这个时候就可以使用goroutine发起n个并发请求,并将请求的结果通过userChannel发送给main线程。main线程收集完n个用户信息后,再做其他业务处理。

但在实际的应用场景中,<em>往往第三方api或网络环境并不是100%可靠的</em>。在上图的示例中,只要有一个goroutine超时,mian线程会由于未收集完所有用户信息而一直等待造成阻塞。为了解决这个问题,我们可以上述例子中引入time.After。

如下图红框所示,再创建一个二级的goroutine和一个临时的tmpChannel。二级的goroutine用于请求api并将结果放入tmpChannel。一级goroutine使用select监听tmpChannel是否有数据,有数据的情况下将数据发送给main线程。如果监听超过1s(认为api超时),则将nil发送给main线程,作为获取该id用户信息失败的标识。

上图的示例,其实就是一个最初级版本的“隔离熔断器”模型。<em>将不可靠的外部依赖交由子goroutine执行,并监控其执行情况</em>。在外部依赖故障的情况下,执行“降级操作”(case <-time.After部分),以降低故障的外部依赖对main逻辑的影响。这些外部依赖可以是各种各样的网络/io请求:mysql/redis/http等等。

而在“降级操作”中,我们还可以再添加channel用于发送错误统计数据,并使用一个单独的master goroutine接收错误统计数据并作分析:

<ol><li>

在n秒内外部依赖超时m次时,可以标识该外部依赖不可用,下一次需要请求api时,跳过请求逻辑直接执行“降级操作”

</li><li>

间隔k秒后再将该外部依赖标识为可用,反复监控其状态。

</li></ol>

以上都是一个完备的隔离熔断器所具备的功能点,读者有兴趣的也可以找一些相关的源码来看,这里不再展开赘述。

2.并发控制

一个能处理并发请求的web服务通常都不可避免的需要将请求压力下方至基础服务层。但基础服务往往承载n多个上层业务,基础服务一旦抗不住down掉了极有可能导致一连串的上层服务雪崩。除了基础业务本身应提高自身可支撑的并发量外,<em>其实上层业务也有义务在尽可能的情况下,控制下放到基础服务的并发请求量,起消峰填谷的作用</em>。下面的示例,是一个简单的并发控制器,用到了同步锁和channel。

<pre class="has"><code class="language-go">// 1.定义并发控制器 type ConcurrentCtrl struct {     lock *sync.Mutex //操作本示例同步锁     maxNum     int //最大同时执行任务数     currentNum int //当前执行任务数     jobNum        int      //任务等待队列长度     jobQueueStart *jobNode //队列头     jobQueueEnd   *jobNode //队列尾     addJob    chan *jobNode //添加任务channel通知     finishJob chan bool     //完成任务channel通知 } // 2.使用的时候,对控制器初始化并创建启用一个master的goroutine func NewConcurrentCtrl(maxNum int) *ConcurrentCtrl {     pool := &ConcurrentCtrl{         maxNum:    maxNum,         addJob:    make(chan *jobNode),         finishJob: make(chan bool),         lock:      new(sync.Mutex),     }     go pool.startMaster() //管理控制器的独立goroutine     return pool } // 3.管理器逻辑 func (p *ConcurrentCtrl) startMaster() {     for {         select {         case newJob := <-p.addJob:             // 收到一个新任务, 先检查是否可以创建新goroutine             if p.addCurrentNum() {                 go p.runJob(newJob)             } else {                 //不能创建goroutine,排队                 p.pushNode(newJob)             }         case <-p.finishJob:             //一个任务结束, 检查队列中是否有任务; 有, 创建goroutine执行; 没有, 减currentNum             if node := p.popNode(); node != nil {                 go p.runJob(node)             } else {                 p.subCurrentNum()             }         }     } } </code></pre>

管理器通过监听addJob和finishJob两个channel, 控制正在执行的任务数。当控制器goroutine监听到添加任务的消息时,先检查正在执行的任务数,正在执行任务数小于最大任务数时,正在执行任务数 1,并创建子goroutine处理任务逻辑。当子goroutine执行完任务逻辑后,发送finishJob通知控制器。控制器监听到完成任务的消息时,检查是否有正在排队的任务,如果有,直接执行,否则执行任务数-1。如下图所示,红色部分为master逻辑。


上面的基础模型,除了可以管理并发任务外,还可以作为长连接管理,例如mysql长连接池(当然具体逻辑要复杂许多)。有兴趣的读者也可以尝试阅读这些源码深度体验channel的应用。

THE END

本期赠书             

《编程的原则:改善代码质量的101个方法》([日]上田勋  著  支鹏浩 译)介绍了软件开发领域 101 个重要的编程原则,涉及编程中的永恒真理,指导方针,编程思想,程序员的视角、习惯和工具,以及编程的反模式等内容。书中以“这个原则是什么”“为什么要遵循这个原则”“具体应该怎么做”为中心,对各个原则进行介绍,简明扼要,通俗易懂。这些原则凝聚了前人的智慧,经过了历史的考验,是指导程序员改善代码、进一步提升编程能力的实用指南。

参与方式

文末留言板留言,点赞前5名各获赠书一本

获奖公布

  公布时间及位置:10月1日头条推送文末

  特别提醒:兑奖截止至10月8日,请参与读者及时兑奖

上期赠书获奖名单公布

恭喜“南吕廿八”、“旅途”、“vil”、“陈有华”、“狼喜欢夏天”!以上读者请添加小编微信:sohu-tech20兑奖~

加入搜狐技术作者天团

千元稿费等你来!

戳这里!☛

也许你还想看

(▼点击文章标题或封面查看)

 

【文末有惊喜!】DLNA技术初探

 

探秘 App Clips

 

全面详细的java线程池解密,看我就够了!

到此这篇关于“【文末有惊喜!】一文读懂golang channel”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
【文末有惊喜!】一文读懂golang channel
golang channel的使用以及调度原理
Go并发编程——channel
协程调度时机二:Channel读写
Go语言并发模型:以并行处理MD5为例
使用Go语言一段时间的感受
图解Go的channel底层原理
Golang并发:并发协程的优雅退出
Go 面试每天一篇(第 39 天)
一分钟系列: Go 编程之用时定义

[关闭]
~ ~