加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

channel数据结构

发布时间:2020-12-15 04:51:02 所属栏目:百科 来源:网络整理
导读:Go语言channel是first-class的,意味着它可以被存储到变量中,可以作为参数传递给函数,也可以作为函数的返回值返回。作为Go语言的核心特征之一,虽然channel看上去很高端,但是其实channel仅仅就是一个数据结构而已,结构体定义如下: struct Hchan { uintg

Go语言channel是first-class的,意味着它可以被存储到变量中,可以作为参数传递给函数,也可以作为函数的返回值返回。作为Go语言的核心特征之一,虽然channel看上去很高端,但是其实channel仅仅就是一个数据结构而已,结构体定义如下:


struct Hchan


{


uintgo qcount; // 队列q中的总数据数量


uintgo dataqsiz; // 环形队列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;


};


让我们来看一个Hchan这个结构体。其中一个核心的部分是存放channel数据的环形队列,由qcount和elemsize分别指定了队列的容量和当前使用量。dataqsize是队列的大小。elemalg是元素操作的一个Alg结构体,记录下元素的操作,如copy函数,equal函数,hash函数等。


可能会有人疑惑,结构体中只看到了队列大小相关的域,并没有看到存放数据的域啊?如果是带缓冲区的chan,则缓冲区数据实际上是紧接着Hchan结构体中分配的。


c = (Hchan*)runtime.mal(n + hintelem->size);


另一个重要部分就是recvq和sendq两个链表,一个是因读这个通道而导致阻塞的goroutine,另一个是因为写这个通道而阻塞的goroutine。如果一个goroutine阻塞于channel了,那么它就被挂在recvq或sendq中。WaitQ是链表的定义,包含一个头结点和一个尾结点:


struct WaitQ


{


SudoG first;


SudoG* last;


};


队列中的每个成员是一个SudoG结构体变量。


struct SudoG


{


G* g; // g and selgen constitute


uint32 selgen; // a weak pointer to g


SudoG* link;


int64 releasetime;


byte* elem; // data element


};


该结构中主要的就是一个g和一个elem。elem用于存储goroutine的数据。读通道时,数据会从Hchan的队列中拷贝到SudoG的elem域。写通道时,数据则是由SudoG的elem域拷贝到Hchan的队列中。


Hchan结构如下图所示:


读写channel操作


先看写channel的操作,基本的写channel操作,在底层运行时库中对应的是一个runtime.chansend函数。


c <- v


在运行时库中会执行:


void runtime·chansend(ChanType *t,Hchan *c,byte *ep,bool *pres,void *pc)


其中c就是channel,ep是取变量v的地址。这里的传值约定是调用者负责分配好ep的空间,仅需要简单的取变量地址就够了。pres参数是在select中的通道操作使用的。


这个函数首先会区分是同步还是异步。同步是指chan是不带缓冲区的,因此可能写阻塞,而异步是指chan带缓冲区,只有缓冲区满才阻塞。


在同步的情况下,由于channel本身是不带数据缓存的,这时首先会查看Hchan结构体中的recvq链表时否为空,即是否有因为读该管道而阻塞的goroutine。如果有则可以正常写channel,否则操作会阻塞。


recvq不为空的情况下,将一个SudoG结构体出队列,将传给通道的数据(函数参数ep)拷贝到SudoG结构体中的elem域,并将SudoG中的g放到就绪队列中,状态置为ready,然后函数返回。


如果recvq为空,否则要将当前goroutine阻塞。此时将一个SudoG结构体,挂到通道的sendq链表中,这个SudoG中的elem域是参数eq,SudoG中的g是当前的goroutine。当前goroutine会被设置为waiting状态并挂到等待队列中。


在异步的情况,如果缓冲区满了,也是要将当前goroutine和数据一起作为SudoG结构体挂在sendq队列中,表示因写channel而阻塞。否则也是先看有没有recvq链表是否为空,有就唤醒。


跟同步不同的是在channel缓冲区不满的情况,这里不会阻塞写者,而是将数据放到channel的缓冲区中,调用者返回。


读channel的操作也是类似的,对应的函数是runtime.chansend。一个是收一个是发,基本的过程都是差不多的。


需要注意的是几种特殊情况下的通道操作–空通道和关闭的通道。


空通道是指将一个channel赋值为nil,或者定义后不调用make进行初始化。按照Go语言的语言规范,读写空通道是永远阻塞的。其实在函数runtime.chansend和runtime.chanrecv开头就有判断这类情况,如果发现参数c是空的,则直接将当前的goroutine放到等待队列,状态设置为waiting。


读一个关闭的通道,永远不会阻塞,会返回一个通道数据类型的零值。这个实现也很简单,将零值复制到调用函数的参数ep中。写一个关闭的通道,则会panic。关闭一个空通道,也会导致panic。


select的实现


select-case中的chan操作编译成了if-else。比如:


select {


case v = <-c:


…foo


default:


…bar


}


会被编译为:


if selectnbrecv(&v,c) {


…foo


} else {


…bar


}


类似地

select {


case v,ok = <-c:


… foo


default:


… bar


}


会被编译为:


if c != nil && selectnbrecv2(&v,&ok,c) {


… foo


} else {


… bar


}


接下来就是看一下selectnbrecv相关的函数了。其实没有任何特殊的魔法,这些函数只是简单地调用runtime.chanrecv函数,只不过设置了一个参数,告诉当runtime.chanrecv函数,当不能完成操作时不要阻塞,而是返回失败。也就是说,所有的select操作其实都仅仅是被换成了if-else判断,底层调用的不阻塞的通道操作函数。


在Go的语言规范中,select中的case的执行顺序是随机的,而不像switch中的case那样一条一条的顺序执行。那么,如何实现随机呢?


select和case关键字使用了下面的结构体:


struct Scase


{


SudoG sg; // must be first member (cast to Scase)


Hchan* chan; // chan


byte* pc; // return pc


uint16 kind;


uint16 so; // vararg of selected bool


bool* receivedp; // pointer to received bool (recv2)


};

struct Select


{


uint16 tcase; // 总的scase[]数量


uint16 ncase; // 当前填充了的scase[]数量


uint16* pollorder; // case的poll次序


Hchan** lockorder; // channel的锁住的次序


Scase scase[1]; // 每个case会在结构体里有一个Scase,顺序是按出现的次序


};


每个select都对应一个Select结构体。在Select数据结构中有个Scase数组,记录下了每一个case,而Scase中包含了Hchan。然后pollorder数组将元素随机排列,这样就可以将Scase乱序了。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读