百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

深入理解Golang通道

ztj100 2025-01-10 18:40 14 浏览 0 评论

Golang是一种易于理解且快捷方便的编程语言,当然他也确实存在不好理解的,让几乎所有人都迷惑且伤脑的部分——Golang通道。

在实际操作时候基本上可以想象通道为锁定的、缓冲的队列。其原理为API与队列匹配,由队列实现,类比队列的抽象最容易编写和读取通道代码。

但是仅仅了解通道API的工作原理还不够。如果要能正确地、灵活地、高效地用好通道生态系统及其威力。通常还取决于等待组,错误组,go协程和其他并发原语。了解Golang运行时如何管理通道很重要。在某些情况下,通道会添加不必要的内容与互斥体或原子相比的开销。

本文虫虫将带大家一起深入探讨这些主题,以便可以更好理解Go通道,并最终善用它。

队列锁

通道被实现为具有锁保护访问的队列。发送者将其添加到队列中。接收者从队列中读取。队列的大小总是固定的。发送者可能会遇到队列堆积满员,需要等待添才能添加,接收者也可以堆积竞争下一个项目。发送者和接收队列(与消息队列类比),链接到队列列表。Go运行时手动停止并向发送器和接收器发出信号。例如,避免发送者在满员队列上浪费时间,而接收器可以正常工作。

以下是将工作发送到通道的方法:

func send(w Work, ch chan Work) {
ch <- w
}

以下是从通道中读取信息的方法:

func recv(ch chan Work) (Work, bool) {
w, ok := <-ch
return w, ok
}

类似于从队列中添加/删除的方法:

func (q *Queue) send(w Work) {
q = append(q, w)
}
func (q *Queue) recv() (Work, bool) {
if len(q) == 0 {
return nil, false
}
w := q[0]
q = q[1:]
return w, true
}

由于通道操作可能会阻塞,因此selec语句用于在通道满员时扩展选择。下面是在发送消息到通道,但如通道满员导致发送失败:

func trySend(w Work, ch chan Work) (success bool) {
select {
case ch <-w:
return true
default:
return false
}
}

同样,也可以提供额外的通道来提供缓冲阻塞:

func sendWithEscapeHatch(w Work, ch chan Work, done chan struct{}) bool {
select {
case ch <- w:
return true
case <-done:
return false
}
}
func escapeHatch(done chan struct{}) {
close(done)
}

全通道实施有很多微妙的细节,但是类别队列的行为,对通道理解很有帮助:

写入已满的队列-> 发送者排队等待

从空队列中读取 -> 接受者排队等待

从关闭的队列中读取 -> 读取值直到为空

写入封闭队列 -> 应用程序做错了什么,要么错过了工作,要么程序应该中断编写器

关闭一个零通道或之前关闭的通道 -> 应用程序应该做错了什么

并发原语

思考通道的第二种方式不是将其视为孤立的抽象,作为Golang生态系统的基本组成部分并发控制。要使用通道编写并发代码,需要了解其基本几种并发原语。

Go协程

研究以下代码示例:

go func() {
fmt.Println("chongchong 1")
}()
go func() {
fmt.Println("chongchong 2")
}()

试问先打印“chongchong 1”还是“chongchong 2”呢,这是不确定的。因为这两个函数都在新线程中运行与原始调用上下文分开。

在使用通道的代码中经常在单独的Go协程运行send和recv操作,因为Go运行时根据工作的构建位置来切换上下文。

ch := make(chan Work)
go recv(ch)
for _, w := range work {
go send(w, ch)
}

定时器

超时和定时器是两个使用基于时间的逻辑扩展选择块的方法,以下示例中使用一个ticker定时器每秒发送一个任务。

ticker := time.NewTicker(1 * time.Second)
for _, w := range work {
select {
case <-ticker.C
ch <- w
}
}

用超时可以做同样的事情:

for _, w := range work {
select {
case <-time.After(1 * time.Second):
ch <- w
}
}

计时器可以对通道的吞吐量进行速率限制,并且计时器可以用于中断需要很长时间才能完成的工作。

等待组

等待组和错误组有两种协调线程关闭的方法。前面代码中已经使用过done模式,它可以用来杀死掉队的发送者/接收者运行时在后续步骤中在退出:

select {
case ch <- w:
return true
case <-done:
return false
}

如果提前知道工作量并且不关心错误,等待组阻塞主进程,直到出现一组固定的并发线程才结束:

func sumRange(n int) int {
var wg sync.WaitGroup
work := make(chan int)
res := make(chan int)
go func() {
var sum int
for {
select {
case i, ok := <-work:
if !ok {
res <- sum
return
}
sum += i
}
}
}()
for i := 0; i < n; i++ {
wg.Add(1) // 增加任务计数
i := i
go func() {
work <- i
wg.Done() // 减少任务计数
}()
}
wg.Wait() // 在计数为零时候解锁
close(work) // 等发送者完成,再安全退出
return <-res //队列清空后res
}

将发送者和接收者分两个池。发送者从WaitGroup递增和递减以表示正在进行的任务。接收者是一个单一的go协程,它接受来自发送者的任务。当发送者完成发送工作后,wg.Wait()解锁。这时,不会再有工作将添加到队列中,因此可以关闭任务通道。最后,使用一个结果通道res告诉接收者在返回之前结束添加值。

错误组

上面处理中,应用程序提前了解其关闭例程,并使用done和等待组处理,这样的情况在实际中其实很难简单。根据经验,关闭时候通常会发生不可预测的情况,需要组合原语以优雅地完成以下三个工作:

(a)关停发送者,(b)刷新队列, (c)关停接收者。

队列再次分为发送者和执行者。现在发送者通过错误组eg而不是等待组来协调 与彼此。接收者线程仍然从队列中提取任务积累结果。结束也类似。finalize()阻塞直到正在进行的发送者完成所有的工作。发送者完成后,我们就可以安全地 close(c.work)。还有接收者将在清空队列后用结果通道占位填充。

和等待组的方法的主要区别在于这个方法支持有无限的任务。

无限制的工作当也会引发许多失败案例。如果ctx被取消,<-ctx.Done()会立即返回。发送者和接收器会提前关闭,错误组将不接受新的工作,并且调用 finalize将返回遇到的第一个错误。

在后一个示例中,还可以通过其他方式来管理关闭。可以有两个单独的错误组来跟踪发送者和接收者。可以使用done接收器错误向发送器发出信号停止通道(或至少避免阻塞)。

关于如何选择正确的组织模式和原语组合可能是一门艺术,需要大家根据自己的实际任务和经验来抉择。

消息传递

最后说下Goer广为流传的一句话:“以消息传递方式共享内存,不要以共享内存方式传递消息”。

消息传递与共享内存不同。复制值发送方/接收方堆栈之间的共享比共享内存更安全。但它是也可以将带有指针的值发送到通道中。Golang对运行时的控制go协程使通道实现非常高效且是自动实现的。

Go的运行时可以暂停和恢复线程取决于有工作要做的地方。并发增加了一个上下文切换的固定开销,但是对于用户队列实现具有同样的性能。将通道嵌入到语言运行时也使得更容易编写更积极的并发代码。通道生态系统使得其更难学,但它也可以让我们做更多的事情。

Go运行时的智能上下文切换使之成为可能最大限度地提高网络通话之间的生产效率。

总结

通道作为队列抽象最容易理解。但仅靠通道是不够的。实际应用取决于Golang 中的一组并发原语。最后,通道通常抽象细节,使开发人员的生活更轻松也比其他并发选项性能更高。所有这些抽象都是有用的,但它们通常适用于不同的情况。

相关推荐

使用Python编写Ping监测程序(python 测验)

Ping是一种常用的网络诊断工具,它可以测试两台计算机之间的连通性;如果您需要监测某个IP地址的连通情况,可以使用Python编写一个Ping监测程序;本文将介绍如何使用Python编写Ping监测程...

批量ping!有了这个小工具,python再也香不了一点

号主:老杨丨11年资深网络工程师,更多网工提升干货,请关注公众号:网络工程师俱乐部下午好,我的网工朋友。在咱们网工的日常工作中,经常需要检测多个IP地址的连通性。不知道你是否也有这样的经历:对着电脑屏...

python之ping主机(python获取ping结果)

#coding=utf-8frompythonpingimportpingforiinrange(100,255):ip='192.168.1.'+...

网站安全提速秘籍!Nginx配置HTTPS+反向代理实战指南

太好了,你直接问到重点场景了:Nginx+HTTPS+反向代理,这个组合是现代Web架构中最常见的一种部署方式。咱们就从理论原理→实操配置→常见问题排查→高级玩法一层层剖开说,...

Vue开发中使用iframe(vue 使用iframe)

内容:iframe全屏显示...

Vue3项目实践-第五篇(改造登录页-Axios模拟请求数据)

本文将介绍以下内容:项目中的public目录和访问静态资源文件的方法使用json文件代替http模拟请求使用Axios直接访问json文件改造登录页,配合Axios进行登录请求,并...

Vue基础四——Vue-router配置子路由

我们上节课初步了解Vue-router的初步知识,也学会了基本的跳转,那我们这节课学习一下子菜单的路由方式,也叫子路由。子路由的情况一般用在一个页面有他的基础模版,然后它下面的页面都隶属于这个模版,只...

Vue3.0权限管理实现流程【实践】(vue权限管理系统教程)

作者:lxcan转发链接:https://segmentfault.com/a/1190000022431839一、整体思路...

swiper在vue中正确的使用方法(vue中如何使用swiper)

swiper是网页中非常强大的一款轮播插件,说是轮播插件都不恰当,因为它能做的事情太多了,swiper在vue下也是能用的,需要依赖专门的vue-swiper插件,因为vue是没有操作dom的逻辑的,...

Vue怎么实现权限管理?控制到按钮级别的权限怎么做?

在Vue项目中实现权限管理,尤其是控制到按钮级别的权限控制,通常包括以下几个方面:一、权限管理的层级划分...

【Vue3】保姆级毫无废话的进阶到实战教程 - 01

作为一个React、Vue双修选手,在Vue3逐渐稳定下来之后,是时候摸摸Vue3了。Vue3的变化不可谓不大,所以,本系列主要通过对Vue3中的一些BigChanges做...

Vue3开发极简入门(13):编程式导航路由

前面几节文章,写的都是配置路由。但是在实际项目中,下面这种路由导航的写法才是最常用的:比如登录页面,服务端校验成功后,跳转至系统功能页面;通过浏览器输入URL直接进入系统功能页面后,读取本地存储的To...

vue路由同页面重定向(vue路由重定向到外部url)

在Vue中,可以使用路由的重定向功能来实现同页面的重定向。首先,在路由配置文件(通常是`router/index.js`)中,定义一个新的路由,用于重定向到同一个页面。例如,我们可以定义一个名为`Re...

那个 Vue 的路由,路由是干什么用的?

在Vue里,路由就像“页面导航的指挥官”,专门负责管理页面(组件)的切换和显示逻辑。简单来说,它能让单页应用(SPA)像多页应用一样实现“不同URL对应不同页面”的效果,但整个过程不会刷新网页。一、路...

Vue3项目投屏功能开发!(vue投票功能)

最近接了个大屏项目,产品想在不同的显示器上展示大屏项目不同的页面,做出来的效果图大概长这样...

取消回复欢迎 发表评论: