深入理解 Golang: Channel 管道

Channel 的使用

Channel 声明方法

  • chInt := make(chan int) // unbuffered channel 非缓冲通道
  • chInt := make(chan int, 0) // unbuffered channel 非缓冲通道
  • chInt := make(chan int, 2) // bufferd channel 缓冲通道

Channel 基本用法

  • ch <- x // channel 接收数据 x
  • x <- ch // channel 发送数据并赋值给 x
  • <- ch // channel 发送数据,忽略接受者

如果使用了非缓冲通道,此时向缓冲区塞数据需要有地方能立即接收数据,不然会一致阻塞。原理是此时缓冲区无数据(无缓冲),向 channel 发送数据视为直接发送,即直接发送到正在休眠等待的协程中。

func main() {ch := make(chan string)// 阻塞ch <- "ping"<-ch
}

启动协程来拿数据:

func main() {ch := make(chan string)// 程序能通过go func() {<-ch}()ch<-"ping"
}

内存与通信

不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。主要是为了:

  • 避免协程竞争和数据冲突。
  • 更高级的抽象,降低开发难度,增加程序可读性。
  • 模块之间更容易解耦,增强扩展性和可维护性。

通过共享内存案列:

func watch(p *int) {for {if *p == 1 {fmt.Println("go")break}}
}func main() {i := 0go watch(&i)time.Sleep(time.Second)i = 1time.Sleep(time.Second)
}

通过通信的方式如下:

func watch(c chan int) {if <-c == 1 {fmt.Println("go")}
}func main() {c := make(chan int)go watch(c)time.Sleep(time.Second)c <- 1time.Sleep(time.Second)
}

Channel 的设计

在这里插入图片描述

Channel 在 Go 的底层表示为一个 hchan 结构体:

type hchan struct {/* 缓存区结构开始 */qcount   uint           // total data in the queuedataqsiz uint           // size of the circular queuebuf      unsafe.Pointer // points to an array of dataqsiz elementselemsize uint16elemtype *_type // element type/* 缓存区结构结束*/// 发送队列sendx    uint   // send indexsendq    waitq  // list of send waiters// 接收队列recvx    uint   // receive indexrecvq    waitq  // list of recv waiterslock mutex// 0:关闭状态;1:开启状态closed   uint32
}type waitq struct {first *sudoglast  *sudog
}

数据存放在一个环形缓冲区 Ring Buffer,可以降低内存/GC的开销

关于 c <- “x” 语法糖,channel 数据发送原理
Go 中会把 c<- 编译为 chansend1 方法:

// %GOROOT%src/runtime/chan.go
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {chansend(c, elem, true, getcallerpc())
}

发送数据的 3 种情形:

  1. 缓冲区无数据,向 channel 发送数据视为直接发送,将数据直接拷贝给等待接收的协程的接受量,并唤醒该协程;如果无等待中的接收协程,则将数据放入缓冲区。
  2. 缓冲区有数据但缓冲区未满,则数据存到缓冲区。
  3. 接收队列中无休眠等待的协程,且缓冲区已满,则将数据包装成 sudog,放入 sendq 队列休眠等待,然受给 channel 解锁。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {// ...if c.closed != 0 {unlock(&c.lock)panic(plainError("send on closed channel"))}// 1. 取出接收等待队列 recvq 的协程,将数据发送给它if sg := c.recvq.dequeue(); sg != nil {send(c, sg, ep, func() { unlock(&c.lock) }, 3)return true}// 接收等待队列中没有协程时if c.qcount < c.dataqsiz {// 2. 缓存空间还有余量,将数据放入缓冲区qp := chanbuf(c, c.sendx)if raceenabled {racenotify(c, c.sendx, nil)}typedmemmove(c.elemtype, qp, ep)c.sendx++if c.sendx == c.dataqsiz {c.sendx = 0}c.qcount++unlock(&c.lock)return true}// ...// 3. 休眠等待gp := getg()// 包装为 sudogmysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}// 将数据,协程指针等记录到 mysq 中mysg.elem = epmysg.waitlink = nilmysg.g = gpmysg.isSelect = falsemysg.c = cgp.waiting = mysggp.param = nil// 将 mysg 自己入队c.sendq.enqueue(mysg)// 休眠gp.parkingOnChan.Store(true)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)KeepAlive(ep)// 被唤醒后再维护一些数据,注意,此时的记录的数据已经被拿走if mysg != gp.waiting {throw("G waiting list is corrupted")}gp.waiting = nilgp.activeStackChans = falseclosed := !mysg.successgp.param = nil// ...
}

关于 rec <-c 语法糖,channel 数据接收原理
Go 中会把 <-c 编译为 func chanrecv 方法,具体如下:

  • 编译阶段,rec <- c 转化为 runtime.chanrecv1()
  • 编译阶段,rec, ok <- c 转化为 runtime.chanrecv2()
  • 最终会调用 chanrecv() 方法
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {chanrecv(c, elem, true)
}//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {_, received = chanrecv(c, elem, true)return
}

接收数据的 4 种情形:

  1. 有等待中的发送协程(sendq),但缓冲区为空,从协程接收
  2. 有等待中的发送协程(sendq),但缓冲区非空,从缓存接收
  3. 接收缓存
  4. 阻塞接收
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {// ...if c.closed != 0 {// ...} else {// 1.,2. 接收数据前,已经有协程在休眠等待发送数据if sg := c.sendq.dequeue(); sg != nil {// 在 1 的情况下,缓存为空,直接从 sendq 的协程取数据// 在 2 的情况下,从缓存(缓冲)取走数据后,将 sendq 里的等待中的协程的数据放入缓存,并唤醒该协程// 这就是为什么 sendq 队列中的协程被唤醒后,其携带的数据已经被取走的原因recv(c, sg, ep, func() { unlock(&c.lock) }, 3)return true, true}}// 3. 直接从缓存接收数据if c.qcount > 0 {qp := chanbuf(c, c.recvx)if raceenabled {racenotify(c, c.recvx, nil)}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}// ...// 缓冲区为空,同时 sendq 里也没休眠的协程,则休眠等待gp := getg()mysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}mysg.elem = epmysg.waitlink = nilgp.waiting = mysgmysg.g = gpmysg.isSelect = falsemysg.c = cgp.param = nilc.recvq.enqueue(mysg)// 休眠gp.parkingOnChan.Store(true)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)// ...
}

从 Channel 接收数据的过程中被唤醒,说明之前因为没有数据而休眠等待,当发送方发送数据时,会主动将数据拷贝至接收方本地。

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

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

相关文章

(免费分享)springboot,vue扫码点餐

系统前台客户端部署在微信小程序&#xff0c;主要面向的对象为到店点餐用户&#xff0c;我们要为买家提供一套完整的网上购物服务&#xff0c;内容包括扫码点餐、支付下单、凭取餐码取餐等。 系统后台客户端使用H5端&#xff0c;面向对象为在职员工&#xff0c;为员工提供各种…

7/2~7/4学习成果总结

这几天初步的了解了一下Java&#xff0c;然后写了几道题&#xff1a; 下面总结一下学Java的时候遇到的一易错的小问题以及总结&#xff1a; 1. java里面只能在一个源文件里有一个public类&#xff0c;但是入口main那个可以理解为public static是一个整体也就是不属于public&a…

2023 全球数字经济大会人工智能高峰论坛,和鲸科技入选北京市人工智能行业赋能典型案例

7月&#xff0c;由国家发展改革委、工业和信息化部、科技部、国家网信办、商务部、中国科协联合北京市人民政府共同主办“2023全球数字经济大会”在京召开&#xff0c;本届活动主题为“数据驱动发展&#xff0c;智能引领未来”。其中“人工智能高峰论坛”重点围绕通用人工智能大…

【雕爷学编程】Arduino动手做(150)---旋转角度传感器模块

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

图神经网络:(图像分割)3D人物图像分割

文章说明&#xff1a; 1)参考资料&#xff1a;PYG的文档。超链。斯坦福大学的机器学习课程。超链。(应该要挂梯子)。博客原文。超链。(应该要挂梯子)。原文理论参考文献。超链。提取码8848。 2)我在百度网盘上传这篇文章jupyter notebook和预训练模型。超链。提取码8848. 3)博主…

【Spring Boot】Spring Boot配置文件详情

前言 Spring Boot是一个开源的Java框架&#xff0c;用于快速构建应用程序和微服务。它基于Spring Framework&#xff0c;通过自动化配置和约定优于配置的方式&#xff0c;使开发人员可以更快地启动和运行应用程序。Spring Boot提供了许多开箱即用的功能和插件&#xff0c;包括嵌…

微信小程序 滚动到底部加载新的数据 之后滚动到顶部

1.配置到底部监听 在app.json的window里面加入 里面的300表示距离底部300rpx触发onReachBottom事件 默认50rpx "window": {"onReachBottomDistance": 300}, 2.在数据列表的js页面 /*** 页面上拉触底事件的处理函数*/onReachBottom() {console.log("…

消息中间件面试题详解

RabbitMQ 如何保证消息不丢失 消息的重复消费问题如何解决 rabbitmq中死信交换机&#xff08;RabbitMQ延迟队列有了解吗&#xff09; 延迟队列&#xff1a;进入队列的消息会被延迟消费的队列 场景&#xff1a;超时订单&#xff0c;限时优惠&#xff0c;定时发布 延迟队列 …

【Linux】-第一个小程序(进度条)

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树 &#x1f389;作者宣言&#xff1a;认真写好每一篇博客 &#x1f38a;作者gitee:gitee &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作…

真赞!IDEA中可以这么玩MyBatis,让编码速度飞起!

本篇博客图解 MyBatis Generator 的使用过程&#xff0c;并结合实战说明逆向工程的使用方式。 搭建 MyBatis Generator 插件环境 a. 添加插件依赖 pom.xml <!--mybatis 逆向生成插件--> <plugin><groupId>org.mybatis.generator</groupId><artifac…

iPad平板用的触控笔什么牌子好?主动式电容笔推荐

现在&#xff0c;电容笔已经成为在线办公、在线教育等产业中的热门产品&#xff0c;那么&#xff0c;平替电容笔是否会代替苹果原有的电容笔呢&#xff1f;实际上&#xff0c;你根本不需要花那么多钱去买一个原装的苹果电容笔。一支普通的平替式电容笔只需要一两百元&#xff0…

微分方程应用——笔记整理

首先&#xff0c;根据正常思路走&#xff0c;化简得到式子&#xff1a; 不难发现&#xff0c;设 后面得出该方程的通解&#xff1a; 这里要注意什么等于这个通解 --- z 又因为该曲线过点 所以可以求出c为3 该题虽然简单&#xff0c;但是要注意几个问题&#xff0c;该定…