Go語(yǔ)言channel是first-class的,意味著它可以被存儲(chǔ)到變量中,可以作為參數(shù)傳遞給函數(shù),也可以作為函數(shù)的返回值返回。作為Go語(yǔ)言的核心特征之一,雖然channel看上去很高端,但是其實(shí)channel僅僅就是一個(gè)數(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;
};
讓我們來(lái)看一個(gè)Hchan這個(gè)結(jié)構(gòu)體。其中一個(gè)核心的部分是存放channel數(shù)據(jù)的環(huán)形隊(duì)列,由qcount和elemsize分別指定了隊(duì)列的容量和當(dāng)前使用量。dataqsize是隊(duì)列的大小。elemalg是元素操作的一個(gè)Alg結(jié)構(gòu)體,記錄下元素的操作,如copy函數(shù),equal函數(shù),hash函數(shù)等。
可能會(huì)有人疑惑,結(jié)構(gòu)體中只看到了隊(duì)列大小相關(guān)的域,并沒(méi)有看到存放數(shù)據(jù)的域???如果是帶緩沖區(qū)的chan,則緩沖區(qū)數(shù)據(jù)實(shí)際上是緊接著Hchan結(jié)構(gòu)體中分配的。
c = (Hchan*)runtime.mal(n + hint*elem->size);
另一個(gè)重要部分就是recvq和sendq兩個(gè)鏈表,一個(gè)是因讀這個(gè)通道而導(dǎo)致阻塞的goroutine,另一個(gè)是因?yàn)閷?xiě)這個(gè)通道而阻塞的goroutine。如果一個(gè)goroutine阻塞于channel了,那么它就被掛在recvq或sendq中。WaitQ是鏈表的定義,包含一個(gè)頭結(jié)點(diǎn)和一個(gè)尾結(jié)點(diǎn):
struct WaitQ
{
SudoG* first;
SudoG* last;
};
隊(duì)列中的每個(gè)成員是一個(gè)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è)g和一個(gè)elem。elem用于存儲(chǔ)goroutine的數(shù)據(jù)。讀通道時(shí),數(shù)據(jù)會(huì)從Hchan的隊(duì)列中拷貝到SudoG的elem域。寫(xiě)通道時(shí),數(shù)據(jù)則是由SudoG的elem域拷貝到Hchan的隊(duì)列中。
Hchan結(jié)構(gòu)如下圖所示:
先看寫(xiě)channel的操作,基本的寫(xiě)channel操作,在底層運(yùn)行時(shí)庫(kù)中對(duì)應(yīng)的是一個(gè)runtime.chansend函數(shù)。
c <- v
在運(yùn)行時(shí)庫(kù)中會(huì)執(zhí)行:
void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
其中c就是channel,ep是取變量v的地址。這里的傳值約定是調(diào)用者負(fù)責(zé)分配好ep的空間,僅需要簡(jiǎn)單的取變量地址就夠了。pres參數(shù)是在select中的通道操作使用的。
這個(gè)函數(shù)首先會(huì)區(qū)分是同步還是異步。同步是指chan是不帶緩沖區(qū)的,因此可能寫(xiě)阻塞,而異步是指chan帶緩沖區(qū),只有緩沖區(qū)滿(mǎn)才阻塞。
在同步的情況下,由于channel本身是不帶數(shù)據(jù)緩存的,這時(shí)首先會(huì)查看Hchan結(jié)構(gòu)體中的recvq鏈表時(shí)否為空,即是否有因?yàn)樽x該管道而阻塞的goroutine。如果有則可以正常寫(xiě)channel,否則操作會(huì)阻塞。
recvq不為空的情況下,將一個(gè)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阻塞。此時(shí)將一個(gè)SudoG結(jié)構(gòu)體,掛到通道的sendq鏈表中,這個(gè)SudoG中的elem域是參數(shù)eq,SudoG中的g是當(dāng)前的goroutine。當(dāng)前goroutine會(huì)被設(shè)置為waiting狀態(tài)并掛到等待隊(duì)列中。
在異步的情況,如果緩沖區(qū)滿(mǎn)了,也是要將當(dāng)前goroutine和數(shù)據(jù)一起作為SudoG結(jié)構(gòu)體掛在sendq隊(duì)列中,表示因?qū)慶hannel而阻塞。否則也是先看有沒(méi)有recvq鏈表是否為空,有就喚醒。
跟同步不同的是在channel緩沖區(qū)不滿(mǎn)的情況,這里不會(huì)阻塞寫(xiě)者,而是將數(shù)據(jù)放到channel的緩沖區(qū)中,調(diào)用者返回。
讀channel的操作也是類(lèi)似的,對(duì)應(yīng)的函數(shù)是runtime.chansend。一個(gè)是收一個(gè)是發(fā),基本的過(guò)程都是差不多的。
需要注意的是幾種特殊情況下的通道操作--空通道和關(guān)閉的通道。
空通道是指將一個(gè)channel賦值為nil,或者定義后不調(diào)用make進(jìn)行初始化。按照Go語(yǔ)言的語(yǔ)言規(guī)范,讀寫(xiě)空通道是永遠(yuǎn)阻塞的。其實(shí)在函數(shù)runtime.chansend和runtime.chanrecv開(kāi)頭就有判斷這類(lèi)情況,如果發(fā)現(xiàn)參數(shù)c是空的,則直接將當(dāng)前的goroutine放到等待隊(duì)列,狀態(tài)設(shè)置為waiting。
讀一個(gè)關(guān)閉的通道,永遠(yuǎn)不會(huì)阻塞,會(huì)返回一個(gè)通道數(shù)據(jù)類(lèi)型的零值。這個(gè)實(shí)現(xiàn)也很簡(jiǎn)單,將零值復(fù)制到調(diào)用函數(shù)的參數(shù)ep中。寫(xiě)一個(gè)關(guān)閉的通道,則會(huì)panic。關(guān)閉一個(gè)空通道,也會(huì)導(dǎo)致panic。
select-case中的chan操作編譯成了if-else。比如:
select {
case v = <-c:
...foo
default:
...bar
}
會(huì)被編譯為:
if selectnbrecv(&v, c) {
...foo
} else {
...bar
}
類(lèi)似地
select {
case v, ok = <-c:
... foo
default:
... bar
}
會(huì)被編譯為:
if c != nil && selectnbrecv2(&v, &ok, c) {
... foo
} else {
... bar
}
接下來(lái)就是看一下selectnbrecv相關(guān)的函數(shù)了。其實(shí)沒(méi)有任何特殊的魔法,這些函數(shù)只是簡(jiǎn)單地調(diào)用runtime.chanrecv函數(shù),只不過(guò)設(shè)置了一個(gè)參數(shù),告訴當(dāng)runtime.chanrecv函數(shù),當(dāng)不能完成操作時(shí)不要阻塞,而是返回失敗。也就是說(shuō),所有的select操作其實(shí)都僅僅是被換成了if-else判斷,底層調(diào)用的不阻塞的通道操作函數(shù)。
在Go的語(yǔ)言規(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]; // 每個(gè)case會(huì)在結(jié)構(gòu)體里有一個(gè)Scase,順序是按出現(xiàn)的次序
};
每個(gè)select都對(duì)應(yīng)一個(gè)Select結(jié)構(gòu)體。在Select數(shù)據(jù)結(jié)構(gòu)中有個(gè)Scase數(shù)組,記錄下了每一個(gè)case,而Scase中包含了Hchan。然后pollorder數(shù)組將元素隨機(jī)排列,這樣就可以將Scase亂序了。
更多建議: