Go語言 channel介紹

2018-07-25 14:44 更新

channel數(shù)據(jù)結(jié)構(gòu)

Go語言channel是first-class的,意味著它可以被存儲到變量中,可以作為參數(shù)傳遞給函數(shù),也可以作為函數(shù)的返回值返回。作為Go語言的核心特征之一,雖然channel看上去很高端,但是其實(shí)channel僅僅就是一個數(shù)據(jù)結(jié)構(gòu)而已,結(jié)構(gòu)體定義如下:

struct    Hchan
{
    uintgo    qcount;            // 隊(duì)列q中的總數(shù)據(jù)數(shù)量
    uintgo    dataqsiz;        // 環(huán)形隊(duì)列q的數(shù)據(jù)大小
    uint16    elemsize;
    bool    closed;
    uint8    elemalign;
    Alg*    elemalg;        // interface for element type
    uintgo    sendx;            // 發(fā)送index
    uintgo    recvx;            // 接收index
    WaitQ    recvq;            // 因recv而阻塞的等待隊(duì)列
    WaitQ    sendq;            // 因send而阻塞的等待隊(duì)列
    Lock;
};

讓我們來看一個Hchan這個結(jié)構(gòu)體。其中一個核心的部分是存放channel數(shù)據(jù)的環(huán)形隊(duì)列,由qcount和elemsize分別指定了隊(duì)列的容量和當(dāng)前使用量。dataqsize是隊(duì)列的大小。elemalg是元素操作的一個Alg結(jié)構(gòu)體,記錄下元素的操作,如copy函數(shù),equal函數(shù),hash函數(shù)等。

可能會有人疑惑,結(jié)構(gòu)體中只看到了隊(duì)列大小相關(guān)的域,并沒有看到存放數(shù)據(jù)的域???如果是帶緩沖區(qū)的chan,則緩沖區(qū)數(shù)據(jù)實(shí)際上是緊接著Hchan結(jié)構(gòu)體中分配的。

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

另一個重要部分就是recvq和sendq兩個鏈表,一個是因讀這個通道而導(dǎo)致阻塞的goroutine,另一個是因?yàn)閷戇@個通道而阻塞的goroutine。如果一個goroutine阻塞于channel了,那么它就被掛在recvq或sendq中。WaitQ是鏈表的定義,包含一個頭結(jié)點(diǎn)和一個尾結(jié)點(diǎn):

struct    WaitQ
{
    SudoG*    first;
    SudoG*    last;
};

隊(duì)列中的每個成員是一個SudoG結(jié)構(gòu)體變量。

struct    SudoG
{
    G*    g;        // g and selgen constitute
    uint32    selgen;        // a weak pointer to g
    SudoG*    link;
    int64    releasetime;
    byte*    elem;        // data element
};

該結(jié)構(gòu)中主要的就是一個g和一個elem。elem用于存儲goroutine的數(shù)據(jù)。讀通道時,數(shù)據(jù)會從Hchan的隊(duì)列中拷貝到SudoG的elem域。寫通道時,數(shù)據(jù)則是由SudoG的elem域拷貝到Hchan的隊(duì)列中。

Hchan結(jié)構(gòu)如下圖所示: 

讀寫channel操作

先看寫channel的操作,基本的寫channel操作,在底層運(yùn)行時庫中對應(yīng)的是一個runtime.chansend函數(shù)。

c <- v

在運(yùn)行時庫中會執(zhí)行:

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

其中c就是channel,ep是取變量v的地址。這里的傳值約定是調(diào)用者負(fù)責(zé)分配好ep的空間,僅需要簡單的取變量地址就夠了。pres參數(shù)是在select中的通道操作使用的。

這個函數(shù)首先會區(qū)分是同步還是異步。同步是指chan是不帶緩沖區(qū)的,因此可能寫阻塞,而異步是指chan帶緩沖區(qū),只有緩沖區(qū)滿才阻塞。

在同步的情況下,由于channel本身是不帶數(shù)據(jù)緩存的,這時首先會查看Hchan結(jié)構(gòu)體中的recvq鏈表時否為空,即是否有因?yàn)樽x該管道而阻塞的goroutine。如果有則可以正常寫channel,否則操作會阻塞。

recvq不為空的情況下,將一個SudoG結(jié)構(gòu)體出隊(duì)列,將傳給通道的數(shù)據(jù)(函數(shù)參數(shù)ep)拷貝到SudoG結(jié)構(gòu)體中的elem域,并將SudoG中的g放到就緒隊(duì)列中,狀態(tài)置為ready,然后函數(shù)返回。

如果recvq為空,否則要將當(dāng)前goroutine阻塞。此時將一個SudoG結(jié)構(gòu)體,掛到通道的sendq鏈表中,這個SudoG中的elem域是參數(shù)eq,SudoG中的g是當(dāng)前的goroutine。當(dāng)前goroutine會被設(shè)置為waiting狀態(tài)并掛到等待隊(duì)列中。

在異步的情況,如果緩沖區(qū)滿了,也是要將當(dāng)前goroutine和數(shù)據(jù)一起作為SudoG結(jié)構(gòu)體掛在sendq隊(duì)列中,表示因?qū)慶hannel而阻塞。否則也是先看有沒有recvq鏈表是否為空,有就喚醒。

跟同步不同的是在channel緩沖區(qū)不滿的情況,這里不會阻塞寫者,而是將數(shù)據(jù)放到channel的緩沖區(qū)中,調(diào)用者返回。

讀channel的操作也是類似的,對應(yīng)的函數(shù)是runtime.chansend。一個是收一個是發(fā),基本的過程都是差不多的。

需要注意的是幾種特殊情況下的通道操作--空通道和關(guān)閉的通道。

空通道是指將一個channel賦值為nil,或者定義后不調(diào)用make進(jìn)行初始化。按照Go語言的語言規(guī)范,讀寫空通道是永遠(yuǎn)阻塞的。其實(shí)在函數(shù)runtime.chansend和runtime.chanrecv開頭就有判斷這類情況,如果發(fā)現(xiàn)參數(shù)c是空的,則直接將當(dāng)前的goroutine放到等待隊(duì)列,狀態(tài)設(shè)置為waiting。

讀一個關(guān)閉的通道,永遠(yuǎn)不會阻塞,會返回一個通道數(shù)據(jù)類型的零值。這個實(shí)現(xiàn)也很簡單,將零值復(fù)制到調(diào)用函數(shù)的參數(shù)ep中。寫一個關(guān)閉的通道,則會panic。關(guān)閉一個空通道,也會導(dǎo)致panic。

select的實(shí)現(xiàn)

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相關(guān)的函數(shù)了。其實(shí)沒有任何特殊的魔法,這些函數(shù)只是簡單地調(diào)用runtime.chanrecv函數(shù),只不過設(shè)置了一個參數(shù),告訴當(dāng)runtime.chanrecv函數(shù),當(dāng)不能完成操作時不要阻塞,而是返回失敗。也就是說,所有的select操作其實(shí)都僅僅是被換成了if-else判斷,底層調(diào)用的不阻塞的通道操作函數(shù)。

在Go的語言規(guī)范中,select中的case的執(zhí)行順序是隨機(jī)的,而不像switch中的case那樣一條一條的順序執(zhí)行。那么,如何實(shí)現(xiàn)隨機(jī)呢?

select和case關(guān)鍵字使用了下面的結(jié)構(gòu)體:

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[]數(shù)量
    uint16    ncase;            // 當(dāng)前填充了的scase[]數(shù)量
    uint16*    pollorder;        // case的poll次序
    Hchan**    lockorder;        // channel的鎖住的次序
    Scase    scase[1];        // 每個case會在結(jié)構(gòu)體里有一個Scase,順序是按出現(xiàn)的次序
};

每個select都對應(yīng)一個Select結(jié)構(gòu)體。在Select數(shù)據(jù)結(jié)構(gòu)中有個Scase數(shù)組,記錄下了每一個case,而Scase中包含了Hchan。然后pollorder數(shù)組將元素隨機(jī)排列,這樣就可以將Scase亂序了。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號