channel是Golang中一个非常重要的特性,也是Golang CSP并发模型的一个重要体现。简单来说就是,goroutine之间可以通过channel进行通信。
channel在Golang如此重要,在代码中使用频率非常高,以至于不得不好奇其内部实现。本文将基于go 1.13的源码,分析channel的内部实现原理。
channel的基本使用
在正式分析channel的实现之前,我们先看下channel的最基本用法,代码如下:
make(chan int)c <- 1x := <- cc <- 1x := <- c此外,channel还分为有缓存channel和无缓存channel。上述代码中,我们使用的是无缓冲的channel。对于无缓冲的channel,如果当前没有其他goroutine正在接收channel数据,则发送方会阻塞在发送语句处。
make(chan int, 2)channel对应的底层实现函数
<-<-go tool compile -N -l -S hello.goCompiler Explorer
make(chan int)runtime.makechanc <- 1runtime.chansend1x := <- cruntime.chanrecv1runtime/chan.gochannel的构造
make(chan int)runtime.makechant *chantypesize int*hchanhchan中的所有属性大致可以分为三类:
1. buffer相关的属性。例如buf、dataqsiz、qcount等。 当channel的缓冲区大小不为0时,buffer中存放了待接收的数据。使用ring buffer实现。
2. waitq相关的属性,可以理解为是一个FIFO的标准队列。其中recvq中是正在等待接收数据的goroutine,sendq中是等待发送数据的goroutine。waitq使用双向链表实现。
3. 其他属性,例如lock、elemtype、closed等。
makechanbufferhchanbufferwaitqhchan向channel中发送数据
c <- 1runtime.chansendrecvqrecvqx := <- csendqsudogrecvqsend函数的实现主要包含两点:
memmove(dst, src, t.size)goready(gp, skip+1)recvqdataqsizc.qcount < c.dataqsizsendqgoparkgoreadygoparkgoreadygoparkc <- 1c.lock简单来说,整个流程如下:
1. 检查recvq是否为空,如果不为空,则从recvq头部取一个goroutine,将数据发送过去,并唤醒对应的goroutine即可。
2. 如果recvq为空,则将数据放入到buffer中。
sudogsendq从channel中接收数据的过程基本与发送过程类似,此处不再赘述了。具体接收过程涉及到的buffer的相关操作,会在后面进行详细的讲解。
runtime.mutexruntime.mutexchannel的ring buffer实现
channel中使用了ring buffer(环形缓冲区)来缓存写入的数据。ring buffer有很多好处,而且非常适合用来实现FIFO式的固定长度队列。
在channel中,ring buffer的实现如下:

hchanrecvxsendxsendxrecvxrecvxsendxbuf[recvx]buf[sendx] = xbuffer的写入
当buffer未满时,将数据放入到buffer中的操作如下:
chanbuf(c, c.sendx)c.buf[c.sendx]sendxsendxsendxbuffer的读取
sendqchanrecvrecvxsendqepx := <- cxsgsudogtypedmemmove(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就目前的设计来说也还有更多的优化空间。
参考
