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

286 阅读3分钟

channel执行顺序

这里我们先抛出结论:channel的执行是随机的

想要证明这一点很简单,下面我们使用反证法来证明这一点,现在我们定义如下3个协程,同时再main协程上通过for循环不停的向chan中写数据,如果channel的执行是顺序的,那么下面额例子不会有任何输出;否则,证明channel是随机执行的。

例子:

package main

import (
	"log"
)


func main() {
	c := make(chan int)
	go func() {
		for {
			value:=<-c
			if value%3!=1{
				log.Println("1:",false)
			}
		}
	}()
	go func() {
		for {
			value:=<-c
			if value%3!=2{
				log.Println("2:",false)
			}
		}
	}()
	go func() {
		for {
			value:=<-c
			if value%3!=0{
				log.Println("3:",false)
			}
		}
	}()
	index:=1
	for  {
		c<-index
		index++
	}
}

之前的文章中,我们说明了,channel中recvq,sendq内部的实现是一个链表结构,既然是链表结构,那有一点可以明确,链表的访问必然是有序的,既然都是有序的那为什么会导致channel是随机的?

从源码看chansend()的实现

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 忽略一些安全验证的代码
    ...
    //如果接收队列不为空,则调用send函数
    if sg := c.recvq.dequeue(); sg != nil {
            // Found a waiting receiver. We pass the value we want to send
            // directly to the receiver, bypassing the channel buffer (if any).
            send(c, sg, ep, func() { unlock(&c.lock) }, 3)
            return true
    }
    // 如果chann还有空间,则做一些修改hchan状态的操作
    if c.qcount < c.dataqsiz {
       ...
    }
    ...
    // 如果上面的代码不能执行,则说明还没有接收者,则需要创建接收者
    gp := getg()
    // 下面创建一个sudog,sudog可以理解为协程状态
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
            mysg.releasetime = -1
    }
    // 这个协程关联一个值为ep,ep就是我们使用<-传递的值
    mysg.elem = ep
    mysg.waitlink = nil
    // 将协程状态关联到一个协程
    mysg.g = gp
    mysg.isSelect = false
    // 协程状态关联chan,协程执行时,我们需要通过hchan对象获取一些数据
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    // 将我们刚创建的协程状态,放入sendq队列中
    c.sendq.enqueue(mysg)
    gp.waiting = nil
    gp.activeStackChans = false
    closed := !mysg.success
    gp.param = nil
    mysg.c = nil
    // 释放我们创建临时变量占用的内存
    releaseSudog(mysg)
    ...
    return true
}

上面这段源码主要完成了,以下几件事:

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

从源码从,我们可以看出,chansend虽然是顺序被调用的,但是由于chansend内部的实现是通过协程来完成,如果没有接收者时,需要缓存一些这些值,以便后续协程执行时使用,我们知道协程时无序的,那么chan也无法保证写入的值是顺序被接收者接收的。
但是,当存在接收者时会调用send方法,这将导致数据没有被保存在buf中,那么这是不是说明:如果存在接收者时,channel就是有序的呢?