概述

以下是channel的结构体,在runtime/chan.go中定义:

type hchan struct {
        qcount   uint           // buffer中的元素数量

        dataqsiz uint           // size of the circular queue(buffer的总大小)

        buf      unsafe.Pointer // points to an array of dataqsiz elements(指向buffer的指针)

        closed   uint32         // 为1时表示channel关闭

        recvq    waitq  // list of recv waiters,等待从channel接收数据的goroutine们

        sendq    waitq  // list of send waiters,等待向channel发送数据的goroutine们

        lock mutex      // 锁

        ...
}

!channel structure

一般来说,go channel在做操作之前,会先获取锁,结束以后再解锁。不过有时做一些non-blocking操作时,会不获取锁就检查,以提高效率。

channel可以是同步的(无缓存),也可以是异步的(有缓存)。

同步channel

用以下代码创建一个同步的channel并从channel读取数据:

func main() {
    ch := make(chan bool)
    go func() {
        ch <- true
    }()
    <-ch
}

刚创建的时候,channel的情况如下图。Go不会为同步的channel分配buffer,因此buffer的指针为nil,qcount和dataqsiz都是0。

!new channel

两个goroutine的运行次序是未知的,我们假定main函数先对goroutine进行读取。goroutine对channel进行读操作时,会先进行一些检查:channel是否已关闭,是否有缓存,sendq中是否有goroutine等待发送数据。在本例中,当main函数中对channel进行读取时,channel没有buffer,sendq中也没有goroutine等待发送数据。这种情况下,goroutine会把自己加到channel的recvq中,并阻塞:

!channel read blocks

细节:该goroutine会使用acquireSudog()获取一个sudog结构,并将自己的waiting设置为它

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
        // no sender available: block on this channel.

        gp := getg()				// 获取当前goroutine

        mysg := acquireSudog()			// 获取sudog

        mysg.releasetime = 0 
        if t0 != 0 { 
                mysg.releasetime = -1
        }
        // No stack splits between assigning elem and enqueuing mysg

        // on gp.waiting where copystack can find it.

        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)
        goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)
	...
}

关于sudog的解释:sudog represents a g in a wait list, such as for sending/receiving on a channel. sudog is necessary because the g ↔ synchronization object relation is many-to-many. A g can be on many wait lists, so there may be many sudogs for one g; and many gs may be waiting on the same synchronization object, so there may be many sudogs for one object. sudogs are allocated from a special pool. Use acquireSudog and releaseSudog to allocate and free them.

一段时间后,另一个goroutine向channel发送数据,它会做类似的检查,发现recvq中有个goroutine正在等待,于是将数据写入其stack中,并唤醒它。这是唯一一个go runtime会向另一个goroutine的stack内写入数据的地方。此后,两个goroutine都将返回。

被唤醒的goroutine会继续执行chanrecv函数的剩余部分:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	...
        // someone woke us up

        if mysg != gp.waiting {
                throw("G waiting list is corrupted")
        }
        gp.waiting = nil
        if mysg.releasetime > 0 {
                blockevent(mysg.releasetime-t0, 2)
        }
        closed := gp.param == nil
        gp.param = nil
        mysg.c = nil
        releaseSudog(mysg)
        return true, !closed
}

异步channel(带缓存的channel)

请看以下例子:

func main() {
    ch := make(chan bool, 1)
    ch <- true
    go func() {
        <-ch 
    }()
    ch <- true
 }

两个goroutine的运行次序依然是未知的,我们假定两个元素先被发送到channel中,之后另一个goroutine从channel中读取。channel被创建后如下图。与同步channel的区别是,分配了一个buffer,且dataqsiz为1。

!buffered channel

接下来,main goroutine向channel写入第一个元素。首先做如下检查:检查recvq是否为空;若buffer为空,检查buffer中是否有空闲空间。在本例中,recvq中没有goroutine,buffer仍然有1个空位,因此向buffer写入数据,并返回。此时,channel的情况如下图:

!buffered channel with data

下一步,main goroutine向channel发送第二个元素,此时buffer已满。当buffer已满时,有缓存的channel与无缓存的行为一致,main goroutine将自己放入sendq中并阻塞,如下图:

!sendq

最后,另一个goroutine从channel中接收数据。这时问题来了,Go保证channel以FIFO的方式工作,所以它不能从buffer获取数据后就继续执行,否则main goroutine将永远阻塞。为了处理这个问题,它会在从buffer读取数据后,将sendq中的第一个goroutine要写的数据加入到buffer中,并将其从sendq中移除并唤醒。

Select

我们知道,Go支持用select语句操作channel:

select {
case <-ch:
    foo()
default:
    bar()
}

按照我们之前的分析,如果channel无缓存,或者有缓存但是里面没有元素,那么试图从channel读取数据的goroutine将会阻塞。那还怎么执行select语句的default情况呢?我们来看一下从channel接收数据的函数:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)

其中,c指向channel的struct;ep指向一块内存,从channel中读出的数据应该写到这里;若block == false,将使用非阻塞模式。非阻塞模式下,goroutine检查buffer,以及sendq,如果读到了元素,就写入ep中,如果没有,就立刻返回(false, false)。buffer和queue的检查使用了原子操作,而没有使用锁。

关闭channel

关闭channel时,go遍历channel的队列中所有发送者和接受者,并解锁它们。所有接收者会得到该类型的默认值,所有发送者会panic。

相关链接

  • Golang: channels implementation:http://dmitryvorobev.blogspot.hk/2016/08/golang-channels-implementation.html