上篇文章中我们提到了,当runtime.chansend本调用也就是向chan中写入数据时,chansend内部会判断当前chan对象中是否存在接收者,如果存在,则调用send函数,那send函数具体都干了写什么?
send源码分析
按照惯例,因为源码中牵扯go其它方面的知识太多,本文只针对channel做研究,所以我们必须舍弃一些代码,才能让channel的实现更容易被理解。
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// ....
// 这里elem执行接收者用来接收chan数据的内存地址
if sg.elem != nil {
// 将发送的数据复制到elem中,因为ep与sg属于两个不同的协程(内存空间中)
sendDirect(c.elemtype, sg, ep)
// 没有理解这句话
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 准备协程,并将协程推送到协程队列中,协程队列由协程管理器自行调度,这也是为什么channel不是顺序的原因
goready(gp, skip+1)
}
从这段代码中可以看出:
- send负责将两个协程间的数据进行复制
- 并协程推送到协程堆栈上
chanrecv源码解读
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// ...
// 如果存在接收者,则调用recv函数将接收者对应的协程推送到协程堆栈上
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// 如果存在未发送的数据,则
if c.qcount > 0 {
// ...
}
if !block {
unlock(&c.lock)
return false, false
}
// 如果上面的代码不能执行,则说明还没有发送者,则需要创建发送者
gp := getg()
// 下面创建一个sudog,sudog可以理解为协程状态
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 这个协程关联一个值为ep,ep就是我们使用<-传递的值
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
// 将协程状态关联到一个协程
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// 添加到接收者队列
c.recvq.enqueue(mysg)
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
success := mysg.success
gp.param = nil
mysg.c = nil
// 释放我们创建临时变量占用的内存
releaseSudog(mysg)
return true, success
}
上面这段源码与chansend不同之处在于它是创建一些列接收者,然后将接收者放入队列的,大致流程如下:
- 如果存在发送队列,则直接将chan收到的数据通过recv函数发送出去
- 如果不存在发送队列,则先将chan收到的数据保存在hchan.buf上并修改hchan对象相应的值
- 创建一个sudog对象,该对象记录了它将要在哪个协程上执行,并记录执行需要的一些状态数据,比如当前关联的chan的值,以及chan对象本身等
- 在创建好sudog对象后,因为此时不存在发送者,所以要进行缓存,此时就将sudog对象放入当前hchan对象对应的recvq队列中
- 清理sudog对象占用的空间,归还内存