chan通道源码
参考链接
环境
- golang-1.17.8
- 系统mac
- 作图工具,在线processon
- 源代码调试工具dlv
GOT
- chan的底层数据结构
- chan的工作流程
- chansend和chanrev的交互流程。
- 环形缓冲区的实现
- 案例的分析学习
数据结构
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
lock mutex
}
- qcount
相当于,底层buf的长度,len,即实际数据元素的个数。
//go:linkname reflectlite_chanlen internal/reflectlite.chanlen
func reflectlite_chanlen(c *hchan) int {
if c == nil {
return 0
}
return int(c.qcount)
}
- dataqsiz
相当于,底层buf的容量
//go:linkname reflect_chancap reflect.chancap
func reflect_chancap(c *hchan) int {
if c == nil {
return 0
}
return int(c.dataqsiz)
}
- buf
一个底层数组,使用sendx和recvx模拟了一个环形队列
,重用已经使用过的空间,循环使用
- elemesize
chan元素的大小
- closed
判断chan是否已经关闭,非0则为关
- elemtype
chan元素类型,主要是用来make(chan) 的时候初始化空间用的
- sendx
写,当前操作要在buf中写入新元素的位置。每次有新值写入后,sendx++,越界判断
- recvx
读,当前操作要读取buf中数据元素的位置。读取完后,recvx++,越界判断
- recvq
接受 等待sudog队列,一个双向链表队列。
type waitq struct {
first *sudog
last *sudog
}
- sendq
发送等待sudog队列
- lock
锁,读写操作的并发保护
例子
通过一个例子我们来了解一下。
func main() {
ch := make(chan string, 2)
go func() {
time.Sleep(1 * time.Minute)
fmt.Println("[go func]", <-ch) // 读出来时C
}()
ch <- "a"
ch <- "b"
ch <- "c" // gopark 执行之后,程序就无法走下一步了。
fmt.Println("[main]", <-ch)
}
- chan通道容量的2,
- 启动goroutine,等待一分钟后,从通道里面读出数据
- 往通道写入数据【a,b】
- 写入【c】是通道阻塞
- 一分钟后打印
[go func] a
[main] b
代码分析
# 查看代码的汇编信息 ,或者dlv
go tool compile -N -l -S main.go
runtime.chansend1(SB)
runtime.chanrecv1(SB)
【发送端】chansend
- 从【recvq】等待队列中去没有取出数据
c.qcount < c.dataqsiz
buf没有满则,将数据【a,b】写入buf,- 此时,buf的,sendx=0,因为buf是环形缓冲区,则sendx==dataqsiz时,sendx=0。
【写入数据-a】
【写入数据-b】
- 写入新数据【c】时,【recvq】等待队列中没有数据, buf也满了,
- 则将该goroutine打包成sudog,放到【sendq】等待队列中,
- 并gopark挂起该sudog,阻塞,等待接收端的goready唤醒,释放。
【写入数据-c】
【接收端】chanrecv
- 一分钟过后
- 从【sendq】等待队列中取出了数据
- 根据
recvx
重buf中取出数据,并将数据给返回变量,将sendq中取出的数据添加到buf[recvx]中,recvx++,sendx=recvx- 环形goready,发送端的等待 sudog
- 【解锁】return
打印【go func a】
【读出数据a-写入数据c】
- 从【sendq】等待队列中没有取出数据
- 从buf中取出数据,【b】,则recvx++,又因为recvx==dataqsiz,则recvx=0,qcount=1,【解锁】return
打印【main b】
【读出数据b】
【环形buf】
recvx=0,sendx=1,
【0】【1】
【c】
chan操作
写入
runtime.chan/chan_send
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
if c.qcount < c.dataqsiz {
// qp := buf[sendx]
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
}
if !block {
unlock(&c.lock)
return false
}
// 省略
mysg := acquireSudog()
c.sendq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
}
- 前置校验
- 【加锁】
- 如果chan已经关闭,则painc【解锁】reutrn
- 如果
【recvq】
等待协程队列里有数据,则直接取出,将数据send给sudog,【解锁】return - 如果
【qcount < dataqsiz】
,buf中有空闲的空间,则将,数据ep
写到buf中 【解锁】 return - 非阻塞
【!block】
,select的操作的是非阻塞的,他是不会往,waitq中写入数据的【解锁】return - 将当前goroutine打包成sudog,添加到sendq等待队列中
- 调用gopark方法挂起goroutine,阻塞等待channel,让出cpu使用权,等待唤醒(等待接收端的唤醒)
func main() {
ch := make(chan string, 2)
go func() {
time.Sleep(1 * time.Minute)
fmt.Println(<-ch)
}()
ch <- "a"
ch <- "b"
ch <- "c" // gopark 执行之后,是否程序就无法走下一步了。
fmt.Println(<-ch)
}
// 当 ch<-c,是,这一行会阻塞,
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
- 释放sudog资源。
读取
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 前置判断条件
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
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-- // buf的中元素数量减1
unlock(&c.lock)
return true, true
}
if !block {
unlock(&c.lock)
return false, false
}
// 代码省略
mysg := acquireSudog()
c.recvq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
return true, success
}
-
前置校验
-
【加锁】
-
如果
【sendq】
等待协程队列里有数据则,(buf此时说明已经len=cap)- 先将sendq队列中数据取出,
- 从buf中取出recvx下标数据,并赋给接受元素,然后将等待队列中取出数据并赋值给buf中的recvx位置,
- goready,唤醒发送端正在阻塞的sudog。
- 【解锁】返回
-
如果
【qcount > 0】
,表示buf中有元素,则取出元素,recvx++,qcount-- 【解锁】 return -
非阻塞
【!block】
,select的操作的是非阻塞的,他是不会往,waitq中写入数据的【解锁】return -
将当前goroutine打包成sudog,添加到recvq等待队列中
-
调用gopark方法挂起goroutine,阻塞等待channel,让出cpu使用权,等待唤醒(等待发送端的环形)
环形缓冲区
环形队列是一种固定长度FIFO数据结构,数据移除,无需数据搬移,空间复用性高。
在这里指的是,hchan的buf,设置buf的大小,然后元素的一直在buf中进进出出。
写入
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 环形buf中还有空间可用的空间
if c.qcount < c.dataqsiz {
qp := chanbuf(c, c.sendx) // qp := buf[c.sendx]
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep) // 将数据ep写入到qp中,
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
}
-
recvq队列中没有阻塞的sudog,可能在buf中,buf中还可以可用空间则,则将要写入的元素,写到 buf[c.sendx]中,
-
sendx++,即下一次要写入的位置,sendx越界判断,则设置为0,收尾相连。
-
qcount++
读取
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
}s
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemmove(c.elemtype, qp, sg.elem)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
- sendq队列中有数据则表示,buf中的数据是满的,
- chansend的写入数据的优先级是【有recvq则直接发送 > 有空闲buf > 写入sendq】
- 当recv被调用时,buf一定是满的。所以这里的操作的qcount是不会减少的,取出元素,并添加新的元素。
chanbuf(c,c.recvx)
取出要发送的元素,qp,typedmemmove(c.elemtype, ep, qp)
, buf中qp的值赋值给 eptypedmemmove(c.elemtype, qp, sg.elem)
,sendq中的sudgo值,赋值给 buf[recvx]即 qp- recvx的下标操作
队列(双向链表)
chan中waitq的作用:
- 档buf满了的时候,新增的请求可以暂时存放到等待队列中,等待下次消费和生产的双方取出队列中数据,
- 当buf满了,还有大量新增的请求是,可以起到削峰作用,挂起当前gorontine,等待另一方goready,然后释放
type waitq struct {
first *sudog
last *sudog
}
总结
- 通过案例和图解分析,大致明白chan的处理流程。
- chan通过,【等待队列】【环形buf】【lock】【gopark+goready】实现数据的,异步和同步传输
- chansend和chanrecv的,处理数据的优先级
- 明白chanrecv和chansend的从队列中取出的处理方式不同
Q&A
- 代码里面的block的代表的是什么?代表是否是阻塞式调用
对chan的非select操作的block则是true,阻塞的,select操作则是false,也就是说的在select中操作,chan的读写是不会进入的waitq的。
- chanrecv和chansend的从队列中取出的处理方式?
- chanrecv从【sendq】队列中取出数据而不是里面接收这个数据,而是先取出【环形buf】中的数据返回,然后将队列中数据再设置到buf中
符合的通道特性,先来先出。
- chansend从【recv】队列中取出数据后,则立马发送给接受者。
说明此时有大量的【recv】空闲者,则不必再放在buf中,优先先发给等待的recv者。
TODO
- goready和gopark的交互,goready如何唤醒正在gopark的sudog呢?
- sudog有是什么?
- gmp的学习