【后端面试总结】golang channel深入理解

news2024/12/23 14:32:20

在Go语言中,Channel是一种用于在goroutine之间进行通信和同步的重要机制。它提供了一种安全、类型安全的方式来传递数据,使得并发编程变得更加直观和简单。本文将详细介绍Golang中Channel的基本概念、创建与关闭、发送与接收操作,以及相关的使用场景和注意事项。另外,Channel本身也是Golang一个很核心的设计理念的良好体现,即:

Do not communicate by sharing memory; instead, share memory by communicating.

( 来源:Share Memory By Communicating - The Go Programming Language)

Golang的Channel基本介绍

Channel的基本概念

Channel是Go语言中的一种特殊类型,它像一个队列一样,遵循先进先出(FIFO)的原则,确保数据的顺序性。每个Channel都有一个指定的类型,只能传递相同类型的数据。Channel是并发安全的,允许多个goroutine同时读写,而不会引发数据竞争。

Channel的主要作用是实现goroutine之间的通信和同步。通过Channel,一个goroutine可以发送数据到另一个goroutine,从而实现数据的交换和共享。

Channel的创建与关闭

在Go中,可以使用内置的make函数来创建一个Channel。创建Channel时需要指定其传递的数据类型,并可以选择性地指定缓冲区大小。

// 创建一个无缓冲的Channel
ch1 := make(chan int) 
// 创建一个容量为10的缓冲Channel
ch2 := make(chan int, 10)

当不再需要向Channel发送数据,并且已经接收完所有数据时,应该关闭Channel。关闭Channel使用close函数。关闭后的Channel不能再发送数据,但可以继续接收已发送的数据,直到Channel为空。

close(ch1)

需要注意的是,关闭一个已经关闭的Channel会引发panic。

Channel的发送与接收操作

Channel的发送和接收操作使用操作符。发送数据到Channel使用ch 语法,从Channel接收数据使用value := 语法。

默认情况下,Channel的发送和接收操作都是阻塞的。即,在发送数据到Channel时,如果接收方没有准备好接收,发送方会被阻塞;同样,在从Channel接收数据时,如果发送方没有发送数据,接收方也会被阻塞。这种阻塞行为可以用于实现同步和协调。

如果需要实现非阻塞的发送和接收操作,可以使用select语句结合default子句。

Channel的使用场景

Channel在并发编程中有广泛的应用场景,包括但不限于:

  1. 消息传递
  2. Channel可以用于在不同的goroutine之间传递数据,实现基本的数据传输。
  3. 任务分发
  4. 可以将任务分发到多个goroutine中并行处理,每个goroutine处理完成后将结果发送回主goroutine或另一个处理结果的goroutine。
  5. 事件通知
  6. Channel可以用于实现事件的发布和订阅模式,当一个事件发生时,通过Channel将事件通知给所有订阅者。
  7. 同步信号
  8. 可以使用Channel作为信号量,当条件满足时,通过Channel发送一个信号,接收方收到信号后继续执行。
  9. 控制并发任务的启动和结束
  10. 通过Channel可以协调多个goroutine的启动和结束,确保它们按照预定的顺序执行。
  11. 限制并发数
  12. 可以使用带有缓冲的Channel来限制同时运行的goroutine数量。
  13. 同步操作
  14. 可以使用Channel来同步多个操作,确保它们按照预定的顺序进行。
  15. 异步处理
  16. Channel支持异步处理模式,即发送方发送数据后不需要等待接收方处理完成即可继续执行其他任务。

Channel实现原理

chan 使用 hchan 表示,它的传参与赋值始终都是指针形式,每个 hchan 对象代表着一个 chan。

  • hchan 中包含一个缓冲区 buf,它表示已经发送但是还未被接收的数据缓存。buf 的大小由创建 chan 时的参数来决定。qcount 表示当前缓冲区中有效数据的总量,dataqsiz 表示缓冲区的大小,对于无缓冲区通道而言 dataqsiz 的值为 0。如果 qcount 和 dataqsiz 的值相同,则表示缓冲区用完了。
  • 缓冲区表示的是一个环形队列 (如果你不熟悉环形队列,可以看一下 https://www.geeksforgeeks.org/circular-queue-set-1-introduction-array-implementation/)。其中 sendx 表示下一个发送的地址,recvx 表示下一个接收的地址。
  • recvq 表示等待接收的 sudog 列表,一个接收语句执行时,如果缓冲区没有数据而且当前没有别的发送者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 对象放到 recvq 中。
  • sendq 类似于 recvq,一个发送语句执行时,如果缓冲区已经满了,而且没有接收者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 放到 sendq 中。
  • closed 表示通道是否已经被关闭,0 代表没有被关闭,非 0 值代表已经被关闭。
  • lock 用于对 hchan 加锁

hchan 则是 channel 在 golang 中的内部实现。其定义如下:

type hchan struct {
	qcount   uint           // buffer 中已放入的元素个数
	dataqsiz uint           // 用户构造 channel 时指定的 buf 大小
	buf      unsafe.Pointer // buffer
	elemsize uint16         // buffer 中每个元素的大小
	closed   uint32         // channel 是否关闭,== 0 代表未 closed
	elemtype *_type         // channel 元素的类型信息
	sendx    uint           // buffer 中已发送的索引位置 send index
	recvx    uint           // buffer 中已接收的索引位置 receive index
	recvq    waitq          // 等待接收的 goroutine  list of recv waiters
	sendq    waitq          // 等待发送的 goroutine list of send waiters

	lock mutex
}

hchan 中的所有属性大致可以分为三类:

  1. buffer 相关的属性。例如 buf、dataqsiz、qcount 等。 当 channel 的缓冲区大小不为 0 时,buffer 中存放了待接收的数据。使用 ring buffer 实现。
  2. waitq 相关的属性,可以理解为是一个 FIFO 的标准队列。其中 recvq 中是正在等待接收数据的 goroutine,sendq 中是等待发送数据的 goroutine。waitq 使用双向链表实现。
  3. 其他属性,例如 lock、elemtype、closed 等。

channel 的 ring buffer 实现

channel 中使用了 ring buffer(环形缓冲区) 来缓存写入的数据。ring buffer 有很多好处,而且非常适合用来实现 FIFO 式的固定长度队列。

在 channel 中,ring buffer 的实现如下:

hchan 中有两个与 buffer 相关的变量: recvx 和 sendx。其中 sendx 表示 buffer 中可写的 index, recvx 表示 buffer 中可读的 index。 从 recvx 到 sendx 之间的元素,表示已正常存放入 buffer 中的数据。

我们可以直接使用 buf[recvx] 来读取到队列的第一个元素,使用 buf[sendx] = x 来将元素放到队尾。

数据发送:

发送数据分三种情况:

  • 有 goroutine 阻塞在 channel 上,此时 hchan.buf 为空:直接将数据发送给该 goroutine。
  • 当前 hchan.buf 还有可用空间:将数据放到 buffer 里面。
  • 当前 hchan.buf 已满:阻塞当前 goroutine。

第一种情况如下。从当前 channel 的等待队列中取出等待的 goroutine,然后调用 send。goready 负责唤醒 goroutine。

第二种情况比较简单。通过比较 qcount 和 dataqsiz 来判断 hchan.buf 是否还有可用空间。除此之后还需要调整一下 sendx 和 qcount。

数据读取:

从nil channel读取会抛异常。

从 closed channel 接收数据,如果 channel 中还有数据,接着走下面的流程。如果已经没有数据了,则返回默认值。使用 ok-idiom 方式读取的时候,第二个参数返回 false。

当前有发送 goroutine 阻塞在 channel 上,buf 已满:

如果buf size是0,则直接从sender读,否则读取队列的头

lock(&c.lock)

if sg := c.sendq.dequeue(); sg != nil {
    // Found a waiting sender. If buffer is size 0, receive value
    // directly from sender. Otherwise, receive from head of queue
    // and add sender's value to the tail of the queue (both map to
    // the same buffer slot because the queue is full).
    recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
    return true, true
}

buf 中有可用数据:

if c.qcount > 0 {
    // Receive directly from queue
    qp := chanbuf(c, c.recvx)
    if raceenabled {
        raceacquire(qp)
        racerelease(qp)
    }
    if ep != nil {
        typedmemmove(c.elemtype, ep, qp)
    }
    typedmemclr(c.elemtype, qp)
    c.recvx++
    if c.recvx == c.dataqsiz {
        c.recvx = 0
    }
    c.qcount--
    unlock(&c.lock)
    return true, true
}

buf为空,阻塞

无缓冲的通道只有当发送方和接收方都准备好时才会传送数据,否则准备好的一方将会被阻塞。

有缓存的channel区别在于只有当缓冲区被填满时,才会阻塞发送者,只有当缓冲区为空时才会阻塞接受者。

关闭channel的操作原则上应该由发送者完成,因为如果仍然向一个已关闭的channel发送数据,会导致程序抛出panic。而如果由接受者关闭channel,则会遇到这个风险。

从一个已关闭的channel中读取数据不会报错。只不过需要注意的是,接受者就不会被一个已关闭的channel的阻塞。而且接受者从关闭的channel中仍然可以读取出数据,只不过是这个channel的数据类型的默认值。我们可以通过指定接受状态位来观察接受的数据是否是从一个已关闭的channel所发送出来的数据。

有缓冲channel和无缓冲channel的应用场景

  • 无缓冲channel:同步消息
  • 有缓冲channel:异步消息

为什么Channel会被设计成向已经关闭的channel发送数据会引发panic

Channel 的基本特性和关闭机制

首先,我们需要了解 Channel 的基本特性和关闭机制。Channel 在 Go 中是一个类型安全的队列,它支持两个基本操作:发送(send)和接收(receive)。发送操作将数据放入 Channel,而接收操作从 Channel 中取出数据。通过关闭 Channel(使用 close 函数),发送者可以通知接收者没有更多的数据将被发送。

关闭 Channel 是一个单向操作,意味着一旦 Channel 被关闭,就不能再向其中发送数据。这是 Channel 设计中的一个关键原则,它确保了数据的发送和接收之间的同步和一致性。

为什么向已关闭的 Channel 写数据会引发 Panic?

向一个已经关闭的 Channel 发送数据会引发 panic,这主要是出于以下几个原因:

  1. 保持数据一致性:一旦 Channel 被关闭,接收者应该能够安全地假设不会有更多的数据被发送。如果允许向已关闭的 Channel 发送数据,这将会破坏这种一致性,导致接收者无法准确地判断 Channel 的状态,从而可能引发数据竞争或其他并发问题。
  2. 避免资源泄漏:Channel 的关闭通常意味着与其相关的资源(如内存和 goroutine)可以被释放。如果允许向已关闭的 Channel 发送数据,这些资源可能无法被及时释放,从而导致资源泄漏。
  3. 简化错误处理:通过引发 panic,Go 语言强制开发者在编写代码时处理向已关闭的 Channel 发送数据的情况。这有助于开发者在开发阶段就发现并修复潜在的错误,从而提高程序的健壮性和稳定性。
  4. 符合直观预期:从直觉上讲,向一个已经关闭的通信管道发送数据是不合理的。Go 语言的设计哲学倾向于直观和简洁,因此将这种操作定义为 panic 符合大多数开发者的直观预期。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2253736.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ消息可靠性保证机制6--可靠性分析

在使用消息中间件的过程中,难免会出现消息错误或者消息丢失等异常情况。这个时候就需要有一个良好的机制来跟踪记录消息的过程(轨迹溯源),帮助我们排查问题。 在RabbitMQ中可以使用Firehose实现消息的跟踪,Firehose可…

RAG评估指南:从检索到生成,全面解析LLM性能评估方法

前言 这一节我们将从时间线出发对RAG的评估方式进行对比,这些评估方式不仅限于RAG流程之中,其中基于LLM的评估方式更加适用于各行各业。 RAG常用评估方式 上一节我们讲了如何用ROUGE 这个方法评估摘要的相似度,由于篇幅限制,没…

高危端口汇总(Summary of High-Risk Ports)

高危端口汇总 能关闭就关闭 💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解…

电子病历静态数据脱敏路径探索

一、引言 数据脱敏(Data Masking),屏蔽敏感数据,对某些敏感信息(比如patient_name、ip_no、ad、no、icd11、drug等等 )通过脱敏规则进行数据的变形,实现隐私数据的可靠保护。电子病历作为医疗领…

黑马微服务开发与实战学习笔记_导论

系列博客目录 文章目录 系列博客目录为什么学微服务?定义 为什么学微服务? 从下图搜索指数可以看出,微服务热度不减 公司中很多微服务的应用。 公司岗位要求中很多微服务的身影。 定义 微服务是一种软件架构风格,它是以专注于…

Python从入门到入狱

Python是从入门到入狱?这个充满调侃意味的说法在程序员圈子里流传甚广。表面看,它似乎是在嘲笑这门语言从简单易学到深陷麻烦的巨大反差,实际上却隐藏着很多值得深思的问题。要解读这个话题,得从Python的特点、使用场景以及潜在风…

网安瞭望台第9期:0day 情报,OAuth 2.0授权流程学习

国内外要闻 Veeam 修补服务提供商控制台关键 RCE 漏洞 Veeam 发布了安全更新以解决影响服务提供商控制台(VSPC)的一个关键漏洞,该漏洞可能为在易受攻击的实例上执行远程代码创造条件。此漏洞被追踪为 CVE-2024-42448,其 CVSS 评分…

Qt复习学习

https://www.bilibili.com/video/BV1Jp4y167R9/?spm_id_from333.999.0.0&vd_sourceb3723521e243814388688d813c9d475f https://subingwen.cn/qt/qt-primer/#1-4-Qt%E6%A1%88%E4%BE%8B https://subingwen.cn/qt/ https://download.qt.io/archive/qt/1.1Qt的特点 1.2QT中的…

视频监控集中管理方案设计:Liveweb视频汇聚方案技术特点与应用

随着科技的发展,视频监控平台在各个领域的应用越来越广泛。然而,当前的视频监控平台仍存在一些问题,如视频质量不高、监控范围有限、智能化程度不够等。这些问题不仅影响了监控效果,也制约了视频监控平台的发展。 为了解决这些问…

【算法】图论——树的重心

目录 题目解析 算法原理 图的存储 算法实现 题目解析 题目解析 给定一颗树,树中包含n个结点(编号)和n-1条无向边。请找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。 什么是重心? 重…

STM32 进阶 定时器 2基本定时器 基本定时器中断案例:LED闪烁

基本定时器 基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动。 这2个定时器是互相独立的,不共享任何资源。 这个2个基本定时器只能向上计数,由于没有外部IO,所以只能计时,不能对外部…

51单片机(STC89C52RC版本)学习笔记(更新中...)

文章目录 参考资料1. 准备工作1.1 win10配置51单片机开发环境1.1 Ubuntu配置51单片机开发环境问题1:mcs51/8051.h依赖于mcs51/lint.h问题2:提示找不到头文件mcs51/8051.h 2. 认识51单片机2.1 STC89C52单片机2.2 管脚图2.3 原理图2.4 按键抖动2.5 头文件说…

USB 声卡全解析:提升音频体验的得力助手

在当今数字化的时代,音频领域的追求愈发多元。无论是热衷聆听高品质音乐的爱好者,还是在专业音频工作中精雕细琢的人士,亦或是在游戏世界里渴望极致音效沉浸的玩家,都始终在寻觅能让音频体验更上一层楼的妙法。而 USB 声卡&#x…

计算机的错误计算(一百七十四)

摘要 探讨 MATLAB 关于计算机的错误计算(一百七十三)中多项式的秦九韶(或Horner)形式的计算误差。 在计算机的错误计算(一百七十三)中,我们讨论了一个多项式的计算误差。本节探讨其对应秦九韶&…

Magento2如何创建CRUD Models

Mageno2 Model的创建不同于其他框架&#xff0c;需要3个不同目录层级的文件 例如需要为表hello_test创建model&#xff1a; 1、app/code/Hello/Test/Model/Test.php <?phpnamespace Hello\Test\Model;class Test extends \Magento\Framework\Model\AbstractModel {protec…

Visual Studio 2022 项目配置常用选项

作为一名C++开发者,经常需要配置第三方库,今天来跟大家截图一下,方便大家快速配置: 头文件包含目录: 或者: 库文件包含目录:

基于Vue实现的移动端手机商城项目 电商购物网站 成品源码

&#x1f4c2;文章目录 一、&#x1f4d4;网站题目 二、✍️网站描述 三、&#x1f4da;网站介绍 四、&#x1f310;网站演示 &#x1f4f8;部分截图 &#x1f3ac;视频演示 五、⚙️网站代码 &#x1f9f1;项目结构 &#x1f492;vue代码预览 六、&#x1f527;完整…

PHP使用RabbitMQ(正常连接与开启SSL验证后的连接)

代码中包含了PHP在一般情况下使用方法和RabbitMQ开启了SSL验证后的使用方法&#xff08;我这边消费队列是使用接口请求的方式&#xff0c;每次只从中取出一条&#xff09; 安装amqp扩展 PHP使用RabbitMQ前&#xff0c;需要安装amqp扩展&#xff0c;之前文章中介绍了Windows环…

uniapp h5 vue3 m3u8 和 mp4 外链视频播放

m3u8视频播放 使用mui-player 和hls.js。 安装npm install mui-player hls.js我的版本是"hls.js": "^1.5.17"和"mui-player": "^1.8.1"使用 页面标签&#xff1a; 引用&#xff1a; 点击目录播放视频&#xff1a; m3u8视频播放&a…

给el-table表头添加icon图标,以及鼠标移入icon时显示el-tooltip提示内容

在你的代码中&#xff0c;你已经正确地使用了 el-tooltip 组件来实现鼠标划过加号时显示提示信息。el-tooltip 组件的 content 属性设置了提示信息的内容&#xff0c;placement 属性设置了提示信息的位置。 你需要确保 el-tooltip 组件的 content 属性和 placement 属性设置正…