這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
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來說,是公平的。
原文連結