這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言經過多年的發展,於最近推出了第一個穩定版本。相對於C/C++來說,Go有很多獨特之出,比如提供了相當抽象的工具,如channel和goroutine。本文主要介紹channel的實現方式。
簡介
channel有四個操作:
- 建立:
c = make(chan int)
- 發送:
c <- 1
- 提取:
i <- c
- 關閉:
close(c)
根據建立方式的不同,channel還可分為有buffer的channel和沒有buffer的channel。buffer的大小由make的第二個參數指定,預設為0,即沒有buffer。建立有buffer的channel的方式是:c = make(chan int, 10)
channel的實現主要在檔案src/pkg/runtime/chan.c裡面。它的資料結構如下:
structHchan{uint32qcount;// total data in the quint32dataqsiz;// size of the circular quint16elemsize;boolclosed;uint8elemalign;Alg*elemalg;// interface for element typeuint32sendx;// send indexuint32recvx;// receive indexWaitQrecvq;// list of recv waitersWaitQsendq;// list of send waitersLock;};
發送流程
Hchan中的兩個WaitQ(recvq和sendq)是兩個隊列,分別儲存等待從該channel提取和發送的goroutine。以向沒有buffer的channel發送為例,
- 如果向該channel發送資料的goroutine發現
recvq不為空白,則從recvq中取出一個goroutine,然後把資料傳給它,發送完成,發送方goroutine可以繼續執行。提取方goroutine則結束block狀態,可以被調度執行。
- 否則,發送方goroutine被存入
sendq隊列,且發送方goroutine進入block狀態,調度演算法選擇其它goroutine執行。
如果channel有buffer,
- 如果buffer裡有空間,則把資料存入buffer,發送完成;如果
recvq隊列裡有等待的goroutine,則取出一個,並將其喚醒,等待調度執行。發送方goroutine繼續執行。
- 如果buffer已滿,則發送方goroutine被存入
sendq隊列,發送方goroutine進入block狀態,調度演算法選擇其它goroutine執行。
如果向已經關閉的channel發送資料,程式會報錯並異常退出。如下面的程式:
package mainfunc main() {c := make(chan int)d := make(chan int)go func() {<-dclose(c)} ()d <- 4c <- 3}
從已經關閉的channel收取資料不會報錯,也不會異常退出,但是我不確定得到什麼樣的值。除此之外,提取和發送的實現基本是相對的,就不再介紹了。
Buffer空間
buffer的空間緊挨著channel,是在建立的channel的時候一起分配的,
c = (Hchan*)runtime·mal(n + hint*elem->size);
其中hint即為buffer的元素個數,會儲存在dataqsiz裡,另外一起管理buffer的還有qcount、sendx和recvx,分別表示buffer裡的元素個數,下一次發送操作存放資料的位置,以及下一次提取資料的位置。這個buffer是個circular buffer。
Channel與Select
channel配合select語句,可以實現multiplex的效果,如:
select {case <-c1:case <-c2:}
c1和c2哪個channel先有資料到達,哪個case先執行;都沒有資料,就block住;都有資料,以一個公平的方式隨機播放一個case執行。select語句本身沒有增加channel的操作方式,但是它本身的實現也很有趣:
- 當select被block住,它所在的goroutine將被掛在多個channel的
sendq或者recvq上。比如上面的例子中,select所在的goroutine將被掛在c1和c2的recvq上,如果這時有另外兩個goroutine同時分別向c1和c2發送資料,那麼它們將操作同一個goroutine(儘管是不同的channel),這種情況下,要麼加鎖,要麼用原子操作。這就是為什麼dequeue裡要使用runtime·cas的原因,雖然調用dequeue之前上鎖了,但那是給sendq/recvq上鎖,不是給goroutine上鎖。
- 不同goroutine裡面的select語句可能操作同一組channel,那麼就有上鎖的必要。Go的實現裡每個channel有自己的鎖,所以select就需要上多個鎖,稍有不慎,可能導致死結。Go的實現是用bubble sort把channel的地址(即
Hchan*)排序,然後依次上鎖。
- 最後就是如何?相對公平。
相對公平的另一個說法就是每個channel被選中的機率是相等的。實現如下:
for(i=0; incase; i++)sel->pollorder[i] = i;for(i=1; incase; i++) {o = sel->pollorder[i];j = runtime·fastrand1()%(i+1);sel->pollorder[i] = sel->pollorder[j];sel->pollorder[j] = o;}
每個迭代做的事情就是在前i個元素裡隨機播放一個放在第i個位置上。這個演算法比programming pearls裡面的難理解,因為每個元素可能被移動多次。我們分兩種情況來討論,對於任意一個位置i,最終落在這個位置的元素可能來自i之前(包括i)或者i之後。
如果是來自與i之前(包括i),那麼它在之後就不能被交換出去。所以它留在位置i的機率為(1/i) * i/(i+1) * (i+1)/(i+2) * ... * (n-1)/n = 1/n。
如果來自i之後(如位置k),那麼在換到i之後,不能有其後的元素再和i交換,所以機率為(1/k) * k/(k+1) * ... * (n-1)/n = 1/n。
由以上兩種情況可知,任何一個元素出現在位置i的機率都是1/n。
因此,按照pollorder的順序依次檢查case是否能夠執行,對於每個case來說,是公平的。
原文連結