深入Golang Channel:实现原理及性能优化建议
Golang的Channel是其CSP并发模型的关键组成部分,也是Goroutine之间通信的桥梁。Channel在Golang中被频繁使用,深入了解其内部实现原理至关重要。本文将基于Go 1.13源码分析Channel的底层实现。
Channel基础用法
在正式分析Channel实现之前,先回顾其基本用法:
package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }
这段代码展示了Channel的两个基本操作:
- 发送操作:
c
- 接收操作:
x :=
Channel分为缓冲Channel和非缓冲Channel。上述代码使用了非缓冲Channel。在非缓冲Channel中,如果当前没有其他Goroutine接收数据,发送方会在发送语句处阻塞。
初始化Channel时可以指定缓冲区大小,例如make(chan int, 2)
指定缓冲区大小为2。在缓冲区未满之前,发送方可以无阻塞地发送数据,无需等待接收方准备好。但如果缓冲区已满,发送方仍然会阻塞。
Channel底层实现函数
在深入Channel源码之前,需要找到Golang中Channel的具体实现位置。使用Channel时,实际上调用的是runtime.makechan
、runtime.chansend
和runtime.chanrecv
等底层函数。
可以使用go tool compile -N -l -S hello.go
命令将代码转换为汇编指令,或者使用在线工具Compiler Explorer (例如:go.godbolt.org/z/3xw5Cj)。通过分析汇编指令,可以发现:
-
make(chan int)
对应runtime.makechan
函数。 c 对应<code>runtime.chansend
函数。x := 对应<code>runtime.chanrecv
函数。
这些函数的实现都位于Go源码的runtime/chan.go
文件中。
Channel构造
make(chan int)
会被编译器转换为runtime.makechan
函数,其函数签名如下:
func makechan(t *chantype, size int) *hchan
其中,t *chantype
是Channel元素类型,size int
是用户指定的缓冲区大小(未指定则为0),返回值是*hchan
。hchan
是Golang中Channel的内部实现结构体,定义如下:
type hchan struct { qcount uint // 缓冲区中已放入元素的数量 dataqsiz uint // 用户构造Channel时指定的缓冲区大小 buf unsafe.Pointer // 缓冲区 elemsize uint16 // 缓冲区中每个元素的大小 closed uint32 // Channel是否关闭,==0表示未关闭 elemtype *_type // Channel元素的类型信息 sendx uint // 缓冲区中发送元素的索引位置(发送索引) recvx uint // 缓冲区中接收元素的索引位置(接收索引) recvq waitq // 等待接收的Goroutine列表 sendq waitq // 等待发送的Goroutine列表 lock mutex }
hchan
中的属性大致分为三类:
-
缓冲区相关属性: 如
buf
,dataqsiz
,qcount
等。当Channel的缓冲区大小不为0时,缓冲区用于存储待接收的数据,使用环形缓冲区实现。 -
等待队列相关属性:
recvq
包含等待接收数据的Goroutine,sendq
包含等待发送数据的Goroutine。waitq
使用双向链表实现。 -
其他属性: 如
lock
,elemtype
,closed
等。
makechan
函数主要进行一些合法性检查和缓冲区、hchan
等属性的内存分配,这里不再深入讨论。
基于hchan
属性的简单分析,可以看出其中有两个重要的组成部分:缓冲区和等待队列。hchan
的所有行为和实现都围绕这两个组成部分展开。
Channel数据发送
Channel的发送和接收过程非常相似。先分析Channel的发送过程(例如c )。
尝试向Channel发送数据时,如果recvq
队列不为空,则会先从recvq
头部取出一个等待接收数据的Goroutine,并将数据直接发送给该Goroutine。代码如下:
package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }
recvq
包含等待接收数据的Goroutine。当一个Goroutine使用接收操作(例如x := )时,如果此时<code>sendq
不为空,则会从sendq
中取出一个Goroutine,并将数据发送给它。
如果recvq
为空,则表示此时没有Goroutine等待接收数据,Channel会尝试将数据放入缓冲区:
func makechan(t *chantype, size int) *hchan
这段代码的功能很简单,就是将数据放入缓冲区。这个过程涉及环形缓冲区的操作,dataqsiz
表示用户指定的缓冲区大小(未指定则默认为0)。
如果使用的是非缓冲Channel或者缓冲区已满(c.qcount == c.dataqsiz
),则会将待发送的数据和当前Goroutine打包成sudog
对象,放入sendq
,并将当前Goroutine设置为等待状态:
type hchan struct { qcount uint // 缓冲区中已放入元素的数量 dataqsiz uint // 用户构造Channel时指定的缓冲区大小 buf unsafe.Pointer // 缓冲区 elemsize uint16 // 缓冲区中每个元素的大小 closed uint32 // Channel是否关闭,==0表示未关闭 elemtype *_type // Channel元素的类型信息 sendx uint // 缓冲区中发送元素的索引位置(发送索引) recvx uint // 缓冲区中接收元素的索引位置(接收索引) recvq waitq // 等待接收的Goroutine列表 sendq waitq // 等待发送的Goroutine列表 lock mutex }
goparkunlock
会解锁输入的互斥锁并挂起当前Goroutine,将其设置为等待状态。gopark
和goready
是成对出现的,是互逆的操作。
从用户角度来看,调用gopark
后,发送数据的代码语句会阻塞。
Channel数据接收
Channel的接收过程与发送过程基本类似,这里不再赘述。接收过程中涉及的缓冲区相关操作会在后面详细描述。
需要注意的是,Channel的整个发送和接收过程都使用了runtime.mutex
进行加锁。runtime.mutex
是runtime相关源码中常用的轻量级锁,整个过程并非最高效的无锁方案。Golang中存在一个关于无锁Channel的issue:go/issues#8899。
Channel环形缓冲区实现
Channel使用环形缓冲区缓存写入的数据。环形缓冲区具有诸多优点,非常适合实现固定长度的FIFO队列。
Channel中环形缓冲区的实现如下:
hchan
中与缓冲区相关的两个变量:recvx
和sendx
。sendx
表示缓冲区中可写的索引,recvx
表示缓冲区中可读的索引。recvx
和sendx
之间的元素表示已正常放入缓冲区的数据。

可以直接使用buf[recvx]
读取队列的第一个元素,使用buf[sendx] = x
将元素放入队列的末尾。
缓冲区写入
当缓冲区未满时,将数据放入缓冲区的操作如下:
package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }
chanbuf(c, c.sendx)
等价于c.buf[c.sendx]
。上述过程很简单,就是将数据复制到缓冲区sendx
位置。
然后,将sendx
移动到下一个位置。如果sendx
到达最后一个位置,则将其设置为0,这是一种典型的首尾相连的方法。
缓冲区读取
当缓冲区未满时,sendq
也必须为空(因为如果缓冲区未满,发送数据的Goroutine不会排队,而是直接将数据放入缓冲区)。此时Channel的读取逻辑chanrecv
比较简单,可以直接从缓冲区读取数据,也是一个移动recvx
的过程,与上面的缓冲区写入基本相同。
当sendq
中有等待的Goroutine时,缓冲区此时一定已满。此时Channel的读取逻辑如下:
func makechan(t *chantype, size int) *hchan
ep
是接收数据的变量对应的地址(例如,在x := 中,<code>ep
就是x
的地址)。sg
表示从sendq
中取出的第一个sudog
。代码中:
-
typedmemmove(c.elemtype, ep, qp)
表示将缓冲区中当前可读的元素复制到接收变量的地址。 -
typedmemmove(c.elemtype, qp, sg.elem)
表示将sendq
中Goroutine等待发送的数据复制到缓冲区。因为后面执行了recv
,所以相当于将sendq
中的数据放在队列的末尾。
简单来说,这里Channel将缓冲区中的第一个数据复制到对应的接收变量,同时将sendq
中的元素复制到队列的末尾,从而实现FIFO(先进先出)。
总结
Channel作为Golang中最常用的设施之一,理解其源码有助于更好地使用和理解Channel。同时,也不要过度迷信和依赖Channel的性能,当前Channel的设计仍有很大的优化空间。
优化建议:
- 使用更轻量级的锁机制或无锁方案,以提高性能。
- 优化缓冲区管理,减少内存分配和复制操作。
Leapcell:Golang Web 应用最佳 Serverless 平台

最后,推荐一个非常适合部署Go服务的平台:Leapcell
- 多语言支持: 支持JavaScript、Python、Go或Rust开发。
- 免费部署无限项目: 只按使用付费,无请求则无费用。
- 极高的性价比: 按需付费,无空闲费用。例如:25美元可支持694万次请求,平均响应时间为60毫秒。
- 流畅的开发者体验: 直观的UI,轻松设置;全自动CI/CD管道和GitOps集成;实时指标和日志,提供可操作的洞察。
- 轻松扩展和高性能: 自动扩展以轻松处理高并发;零运营开销,专注于构建。

更多信息请查看文档!
Leapcell Twitter: https://www.php.cn/link/7884effb9452a6d7a7a79499ef854afd
以上是Go 频道解锁:它们是如何工作的的详细内容。更多信息请关注PHP中文网其他相关文章!

OpenSSL,作为广泛应用于安全通信的开源库,提供了加密算法、密钥和证书管理等功能。然而,其历史版本中存在一些已知安全漏洞,其中一些危害极大。本文将重点介绍Debian系统中OpenSSL的常见漏洞及应对措施。DebianOpenSSL已知漏洞:OpenSSL曾出现过多个严重漏洞,例如:心脏出血漏洞(CVE-2014-0160):该漏洞影响OpenSSL1.0.1至1.0.1f以及1.0.2至1.0.2beta版本。攻击者可利用此漏洞未经授权读取服务器上的敏感信息,包括加密密钥等。

本文演示了创建模拟和存根进行单元测试。 它强调使用接口,提供模拟实现的示例,并讨论最佳实践,例如保持模拟集中并使用断言库。 文章

本文探讨了GO的仿制药自定义类型约束。 它详细介绍了界面如何定义通用功能的最低类型要求,从而改善了类型的安全性和代码可重复使用性。 本文还讨论了局限性和最佳实践

本文讨论了GO的反思软件包,用于运行时操作代码,对序列化,通用编程等有益。它警告性能成本,例如较慢的执行和更高的内存使用,建议明智的使用和最佳

本文讨论了GO中使用表驱动的测试,该方法使用测试用例表来测试具有多个输入和结果的功能。它突出了诸如提高的可读性,降低重复,可伸缩性,一致性和A

本文使用跟踪工具探讨了GO应用程序执行流。 它讨论了手册和自动仪器技术,比较诸如Jaeger,Zipkin和Opentelemetry之类的工具,并突出显示有效的数据可视化


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

Dreamweaver CS6
视觉化网页开发工具

记事本++7.3.1
好用且免费的代码编辑器

安全考试浏览器
Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

SublimeText3 英文版
推荐:为Win版本,支持代码提示!

ZendStudio 13.5.1 Mac
功能强大的PHP集成开发环境