在go语言中channel是核心的数据类型,主要用来解决协程的同步以及协程之间数据共享(数据传递)的问题,本篇主要从底层实现来分析在go语言中channel是如何实现的。

      首先我们看一下channel的数据结构,在源码包中src/runtime/chan.go中定义了channel的数据结构:       

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
}

 从数据结构可以看出channel由队列、类型信息、协程等队列组成。

环形队列:channel内部实现了一个环形队列作为其缓冲区,队列的长度是在创建channel时指定的,下图展示了一个可缓存8个元素的channel。

dataqsiz指示了队列长度为8,即可缓存8个元素;

buf指向队列的内存;

qcount表示队列中还有两个元素;

sendx表示后续写入的数据存储的位置,取值为[0, 8);

recvx表示从该位置读取数据,值为[0, 8);

其中,sendx表示队尾,即数据写入的位置;recvx表示队首,代表数据读取的位置。

等待队列:从channel读取数据时,如果channel缓冲区为空或没有缓冲区,则当前读协程会被阻塞,并被加入recvq队列。向channel写入数据时,如果缓冲区已满或没有缓冲区,则当前写协程会被阻塞,并被加入sendq队列。

下图展示了一个没有缓冲区的管道,有三个协程阻塞等待读数据。

 处于等待队列中的协程会在其他协程操作channel时被唤醒:

        因读阻塞的协程会被向管道写入数据的协程唤醒;

        因写阻塞的协程会被从管道读取数据的协程唤醒。

一般情况下recvq和sendq至少有一个为空。只有一个例外,那就时同一个协程使用select语句向管道一边写入数据,一边读取数据,此时协程会分别位于两个等待队列中。

一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。

elemtype代表类型,用于在数据传递过程中赋值;

elemsize代表类型大小,用于在buf中定位元素的位置。

channel的操作

1)创建channel

创建管道的过程实际上是初始化hchan结构,其中类型信息和缓冲区长度由内置函数make()指定,buf的大小则由元素大小和缓冲区长度共同决定。

ch := make(chan int, 10)

2)向channel中写入数据的过程如下:

        如果缓冲区中有空余位置,则将数据直接写入缓冲区。

        如果缓冲区中没有空余位置,则将当前写协程加入sendq队列,进入睡眠并等待被读唤                    醒。

操作符“<-"表示数据流向,channel在左表示写入数据,在右表示从channel读数据。

ch <- 1 //数据流入channel

协程写入channel时造成阻塞的条件有:

        channel无缓冲区;

        channel的缓冲区已满;

        channel的值为nil;

3)从channel读数据的过程如下:

        如果缓冲区中有数据,则从缓冲区中取出数据,结束读取过程。

        如果缓冲区没有数据,则将读协程加入recvq队列,进入睡眠并等待被写协程唤醒。

d := <- ch  //数据流出channel

协程读取管道时,阻塞的条件有:

        channel无缓冲区;

        channel的缓冲区中无数据;

        channel的值为nil。

4)关闭channel:

        关闭channel时会把recvq中的协程全部唤醒,这些协程获取的数据都为nil。同时会把sendq队列中的协程全部唤醒,但这些协程会触发panic。

        另外,以下操作也会触发panic:

                关闭值为nil的channel;

                关闭已经被关闭的channel;

                向已经关闭的channel写入数据。

close(chan)