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

在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 // bufferelemsize uint16         // buffer 中每个元素的大小closed   uint32         // channel 是否关闭,== 0 代表未 closedelemtype *_type         // channel 元素的类型信息sendx    uint           // buffer 中已发送的索引位置 send indexrecvx    uint           // buffer 中已接收的索引位置 receive indexrecvq    waitq          // 等待接收的 goroutine  list of recv waiterssendq    waitq          // 等待发送的 goroutine list of send waiterslock 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 queueqp := 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.mzph.cn/diannao/62953.shtml

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

相关文章

华为、华三交换机纯Web下如何创关键VLANIF、操作STP参数

华为交换机WEB操作 使用的是真机S5735,目前主流的版本都适用(V1R5~V2R1的就不在列了,版本太老了,界面完全不一样,这里调试线接的console口,电脑的网络接在ETH口) 「模拟器、工具合集」复制整段内…

详解Java数据库编程之JDBC

目录 首先创建一个Java项目 在Maven中央仓库下载mysql connector的jar包 针对MySQL版本5 针对MySQL版本8 下载之后,在IDEA中创建的项目中建立一个lib目录,然后把刚刚下载好的jar包拷贝进去,然后右键刚刚添加的jar包,点击‘添…

网络(TCP)

目录 TCP socket API 详解 套接字有哪些类型?socket有哪些类型? 图解TCP四次握手断开连接 图解TCP数据报结构以及三次握手(非常详细) socket缓冲区以及阻塞模式详解 再谈UDP和TCP bind(): 我们的程序中对myaddr参数是这样…

【笔记】离散数学 1-3 章

1. 数理逻辑 1.1 命题逻辑的基本概念 1.1.1 命题的概念 命题(Proposition):是一个陈述句,它要么是真的(true),要么是假的(false),但不能同时为真和假。例如…

【Linux篇】权限管理 - 用户与组权限详解

一. 什么是权限? 首先权限是限制人的。人 真实的人 身份角色 权限 角色 事物属性 二. 认识人–用户 Linux下的用户分为超级用户和普通用户 root :超级管理员,几乎不受权限的约束普通用户 :受权限的约束超级用户的命令提示符是#,普通用…

【机器学习】机器学习的基本分类-监督学习-决策树-C4.5 算法

C4.5 是由 Ross Quinlan 提出的决策树算法,是对 ID3 算法的改进版本。它在 ID3 的基础上,解决了以下问题: 处理连续型数据:支持连续型特征,能够通过划分点将连续特征离散化。处理缺失值:能够在特征值缺失的…

2023年MathorCup高校数学建模挑战赛—大数据竞赛B题电商零售商家需求预测及库存优化问题求解全过程文档及程序

2023年MathorCup高校数学建模挑战赛—大数据竞赛 B题 电商零售商家需求预测及库存优化问题 原题再现: 电商平台存在着上千个商家,他们会将商品货物放在电商配套的仓库,电商平台会对这些货物进行统一管理。通过科学的管理手段和智能决策&…

cocotb pytest

打印python中的print , 应该使用 pytest -s pytest --junitxmltest_report.xml --htmlreport.html

【Linux】进程间关系与守护进程

🌎进程间关系与守护进程 文章目录: 进程间关系与守护进程 进程组     会话       认识会话       会话ID       创建会话 控制终端     作业控制       作业(job)和作业控制(Job Control)       作业号及作业过程…

QT5.14 QML串口助手

基于 QML的 串口调试助手 这个代码有缺失,补了部分代码 ASCII HEX 工程共享, Qt版本 5.14.1 COM_QML 通过百度网盘分享的文件:COM_QML.zip 链接:https://pan.baidu.com/s/1MH2d6gIPDSoaX-syVWZsww?pwd5tge 提取码:…

IOS ARKit进行图像识别

先讲一下基础控涧,资源的话可以留言,抽空我把它传到GitHub上,这里没写收积分,竟然充值才能下载,我下载也要充值,牛! ARSCNView 可以理解画布或者场景 1 配置 ARWorldTrackingConfiguration AR追…

C语言第十五周课——课堂练习

目录 1.输出特定图形 2.求三个数的最小值 3.思考题 1.输出特定图形 要求&#xff1a;输出下面形状在控制台 * * * * * * * * * * * * * * * #include <stdio.h> int main() {int i, j;// 外层循环控制行数for (i 1; i < 5; i){// 内层循环控制每行的星号个数for (…

数据结构 (20)二叉树的遍历与线索化

一、二叉树的遍历 遍历是对树的一种最基本的运算&#xff0c;所谓遍历二叉树&#xff0c;就是按一定的规则和顺序走遍二叉树的所有节点&#xff0c;使每一个节点都被访问一次&#xff0c;而且只被访问一次。二叉树的遍历方式主要有四种&#xff1a;前序遍历、中序遍历、后序遍历…

sscanf与sprintf函数

本期介绍&#x1f356; 主要介绍&#xff1a;sscanf()、sprintf()这对输入/输出函数&#xff0c;并详细讲解了这两个函数的应用场景。 概述&#x1f356; 在C语言的输出和输入库中&#xff0c;有三对及其相似的库函数&#xff1a;printf()、scanf()、fprintf()、fscanf()、spri…

Linux条件变量线程池详解

一、条件变量 【互斥量】解决了线程间同步的问题&#xff0c;避免了多线程对同一块临界资源访问产生的冲突&#xff0c;但同一时刻对临界资源的访问&#xff0c;不论是生产者还是消费者&#xff0c;都需要竞争互斥锁&#xff0c;由此也带来了竞争的问题。即生产者和消费者、消费…

【错误记录】jupyter notebook打开后服务器错误Forbidden问题

如题&#xff0c;在Anaconda Prompt里输入jupyter notebook后可以打开浏览器&#xff0c;但打开具体项目后就会显示“服务器错误&#xff1a;Forbidden”&#xff0c;终端出现&#xff1a; tornado.web.HTTPError: HTTP 403: Forbidden 查看jupyter-server和jupyter notebook版…

shodan2-批量查找CVE-2019-0708漏洞

声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&#…

PostgreSQL实现透视表查询

PostgreSQL 8.3版本发布时&#xff0c;引入了一个名为tablefunc的新扩展。这个扩展提供了一组非常有趣的函数。其中之一是交叉表函数&#xff0c;用于创建数据透视表。这就是我们将在本文中讨论的内容。 需求说明 解释此函数如何工作的最简单方法是使用带有数据透视表的示例…

使用Tauri创建桌面应用

当前是在 Windows 环境下 1.准备 系统依赖项 Microsoft C 构建工具WebView2 (Windows10 v1803 以上版本不用下载&#xff0c;已经默认安装了) 下载安装 Rust下载安装 Rust 需要重启终端或者系统 重新打开cmd&#xff0c;键入rustc --version&#xff0c;出现 rust 版本号&…

【掩体计划——DFS+缩点】

题目 代码 #include <bits/stdc.h> using namespace std; const int N 1e5 10; vector<vector<int>> g; bool st[N]; int ans 1e9; bool dfs(int f, int u, int dis) {bool is 1;for (auto j : g[u]){if (j f)continue;is & dfs(u, j, dis (g[u].…