在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)