3.10.1 相关结构体定义 go中的channel是可以被存储在变量中,可以作为参数传递给函数,也可以作为函数返回值返回,我们先来看一下channel的结构体定义:
struct Hchan { uintgo qcount; // 队列q中的总数据数量 uintgo dataqsize; // 环形队列q的数据大小 uint16 elemsize; // 当前队列的使用量 bool closed; uint8 elemalign; Alg* elemalg; // interface for element type uintgo sendx; // 发送index uintgo recvx; // 接收index WaitQ recvq; // 因recv而阻塞的等待队列 WaitQ sendq; // 因send而阻塞的等待队列 Lock; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Hchan结构体中的核心部分是存放channel数据的环形队列,相关数据的作用已经在其后做出了备注。在该结构体中没有存放数据的域,如果是带缓冲区的chan,则缓冲区数据实际上是紧接着Hchan结构体中分配的。
另一个重要部分就是recvq和sendq两个链表,一个是因读这个通道而导致阻塞的goroutine,另一个是因为写这个通道而阻塞的goroutine。如果一个goroutine阻塞于channel了,那么它就被挂在recvq或sendq中。WaitQ是链表的定义,包含一个头结点和一个尾结点,该链表中中存放的成员是一个sudoG结构体变量,具体定义如下:
struct SudoG { G* g; // g and selgen constitute uint32 selgen; // a weak pointer to g SudoG* link; int64 releasetime; byte* elem; // data element }; 1 2 3 4 5 6 7 8 该结构体中最主要的是g和elem。elem用于存储goroutine的数据。读通道时,数据会从Hchan的队列中拷贝到SudoG的elem域。写通道时,数据则是由SudoG的elem域拷贝到Hchan的队列中。
Hchan结构如下:
3.10.2 阻塞式读写channel操作 写操作代码如下,其中的c就是channel,v指的是数据:
c <- v 1 事实上基本的阻塞模式写channel操作在底层运行时库中对应的是一个runtime.chansend函数。具体如下:
void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
其中的ep指的是变量v的地址,这里的传值约定是调用者负责分配好ep的空间,仅需要简单的取变量地址就好了,pres是在select中的通道操作中使用的。
阻塞模式读操作的核心函数有两种包装如下:
chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
以及
chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected)
这两种的区别主要在于返回值是否会返回一个bool类型值,该值只是用于判断channel是否能读取出数据。
读写操作的以上阻塞的过程类似,故而不再做出说明,我们补充三个细节:
以上我们都强调是阻塞式的读写操作,其实相对应的也有非阻塞的读写操作,使用过select-case来进行调用的。 空通道,指的是将一个channel赋值为nil,或者调用后不适用make进行初始化。读写空通道是永远阻塞的。 关闭的通道,永远不会阻塞,会返回一个通道数据类型的零值。首先将closed置为1,第二步收集读等待队列recvq的所有sg,每个sg的elem都设为类型零值,第三步收集写等待队列sendq的所有sg,每个sg的elem都设为nil,最后唤醒所有收集的sg。