GO成神之路: 详解channel(四)|Go主题月

456 阅读2分钟

上篇文章中我们提到了,当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)
}

从这段代码中可以看出:

  1. send负责将两个协程间的数据进行复制
  2. 并协程推送到协程堆栈上

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不同之处在于它是创建一些列接收者,然后将接收者放入队列的,大致流程如下:

  1. 如果存在发送队列,则直接将chan收到的数据通过recv函数发送出去
  2. 如果不存在发送队列,则先将chan收到的数据保存在hchan.buf上并修改hchan对象相应的值
  3. 创建一个sudog对象,该对象记录了它将要在哪个协程上执行,并记录执行需要的一些状态数据,比如当前关联的chan的值,以及chan对象本身等
  4. 在创建好sudog对象后,因为此时不存在发送者,所以要进行缓存,此时就将sudog对象放入当前hchan对象对应的recvq队列中
  5. 清理sudog对象占用的空间,归还内存