channel是Golang中一个非常重要的特性,也是Golang CSP并发模型的一个重要体现。简单来说就是,goroutine之间可以通过channel进行通信。
channel在Golang如此重要,在代码中使用频率非常高,以至于不得不好奇其内部实现。本文将基于go 1.13的源码,分析channel的内部实现原理。
channel的基本使用
在正式分析channel的实现之前,我们先看下channel的最基本用法,代码如下:
make(chan int)c <- 1x := <- c
c <- 1x := <- c
此外,channel还分为有缓存channel和无缓存channel。上述代码中,我们使用的是无缓冲的channel。对于无缓冲的channel,如果当前没有其他goroutine正在接收channel数据,则发送方会阻塞在发送语句处。
make(chan int, 2)
channel对应的底层实现函数
<-<-
go tool compile -N -l -S hello.go
Compiler Explorer

make(chan int)runtime.makechanc <- 1runtime.chansend1x := <- cruntime.chanrecv1
runtime/chan.go
channel的构造
make(chan int)runtime.makechan
t *chantypesize int*hchan
hchan中的所有属性大致可以分为三类:
1. buffer相关的属性。例如buf、dataqsiz、qcount等。 当channel的缓冲区大小不为0时,buffer中存放了待接收的数据。使用ring buffer实现。
2. waitq相关的属性,可以理解为是一个FIFO的标准队列。其中recvq中是正在等待接收数据的goroutine,sendq中是等待发送数据的goroutine。waitq使用双向链表实现。
3. 其他属性,例如lock、elemtype、closed等。
makechanbufferhchan
bufferwaitqhchan
向channel中发送数据
c <- 1runtime.chansend
recvqrecvq
x := <- csendqsudog
recvq
send函数的实现主要包含两点:
memmove(dst, src, t.size)
goready(gp, skip+1)
recvq
dataqsiz
c.qcount < c.dataqsizsendq
goparkgoreadygoparkgoready
gopark
c <- 1c.lock
简单来说,整个流程如下:
1. 检查recvq是否为空,如果不为空,则从recvq头部取一个goroutine,将数据发送过去,并唤醒对应的goroutine即可。
2. 如果recvq为空,则将数据放入到buffer中。
sudogsendq
从channel中接收数据的过程基本与发送过程类似,此处不再赘述了。具体接收过程涉及到的buffer的相关操作,会在后面进行详细的讲解。
runtime.mutexruntime.mutex
channel的ring buffer实现
channel中使用了ring buffer(环形缓冲区)来缓存写入的数据。ring buffer有很多好处,而且非常适合用来实现FIFO式的固定长度队列。
在channel中,ring buffer的实现如下:
hchanrecvxsendxsendxrecvxrecvxsendx
buf[recvx]buf[sendx] = x
buffer的写入
当buffer未满时,将数据放入到buffer中的操作如下:
chanbuf(c, c.sendx)c.buf[c.sendx]sendx
sendxsendx
buffer的读取
sendqchanrecvrecvx
sendq
epx := <- cxsgsudog
typedmemmove(c.elemtype, ep, qp)
typedmemmove(c.elemtype, qp, sg.elem)recv++
简单来说,这里channel将buffer中队首的数据拷贝给了对应的接收变量,同时将sendq中的元素拷贝到了队尾,这样可以才可以做到数据的FIFO(先入先出)。
c.sendx = c.recvxc.sendx = (c.sendx+1) % c.dataqsizsendx == recvx
总结
channel作为golang中最常用设施,了解其源码可以帮助我们更好的理解和使用。同时也不会过于迷信和依赖channel的性能,channel就目前的设计来说也还有更多的优化空间。
参考
