Go 频道解锁:它们是如何工作的
深入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 <- 1
- 接收操作:
x := <-c
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 <- 1
对应runtime.chansend
函数。x := <-c
对应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 <- 1
)。
尝试向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 := <-c
)时,如果此时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 := <-c
中,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中文网其他相关文章!

热AI工具

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

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

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

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

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

Go语言在构建高效且可扩展的系统中表现出色,其优势包括:1.高性能:编译成机器码,运行速度快;2.并发编程:通过goroutines和channels简化多任务处理;3.简洁性:语法简洁,降低学习和维护成本;4.跨平台:支持跨平台编译,方便部署。

Golang在并发性上优于C ,而C 在原始速度上优于Golang。1)Golang通过goroutine和channel实现高效并发,适合处理大量并发任务。2)C 通过编译器优化和标准库,提供接近硬件的高性能,适合需要极致优化的应用。

Golang和Python各有优势:Golang适合高性能和并发编程,Python适用于数据科学和Web开发。 Golang以其并发模型和高效性能着称,Python则以简洁语法和丰富库生态系统着称。

Golang在性能和可扩展性方面优于Python。1)Golang的编译型特性和高效并发模型使其在高并发场景下表现出色。2)Python作为解释型语言,执行速度较慢,但通过工具如Cython可优化性能。

Golang和C 在性能竞赛中的表现各有优势:1)Golang适合高并发和快速开发,2)C 提供更高性能和细粒度控制。选择应基于项目需求和团队技术栈。

GoimpactsdevelopmentPositationalityThroughSpeed,效率和模拟性。1)速度:gocompilesquicklyandrunseff,ifealforlargeprojects.2)效率:效率:ITScomprehenSevestAndArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdEcceSteral Depentencies,增强开发的简单性:3)SimpleflovelmentIcties:3)简单性。

C 更适合需要直接控制硬件资源和高性能优化的场景,而Golang更适合需要快速开发和高并发处理的场景。1.C 的优势在于其接近硬件的特性和高度的优化能力,适合游戏开发等高性能需求。2.Golang的优势在于其简洁的语法和天然的并发支持,适合高并发服务开发。

Golang和C 在性能上的差异主要体现在内存管理、编译优化和运行时效率等方面。1)Golang的垃圾回收机制方便但可能影响性能,2)C 的手动内存管理和编译器优化在递归计算中表现更为高效。
