go channel實現

go channel實現

Go語言通過多年的發展,於最近推出了第一個穩定版本。相對於C/C++來講,Go有不少獨特之出,好比提供了至關抽象的工具,如channel和goroutine。本文主要介紹channel的實現方式。golang

簡介

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裏面。它的數據結構以下:工具

struct	Hchan
{
	uint32	qcount;			// total data in the q
	uint32	dataqsiz;		// size of the circular q
	uint16	elemsize;
	bool	closed;
	uint8	elemalign;
	Alg*	elemalg;		// interface for element type
	uint32	sendx;			// send index
	uint32	recvx;			// receive index
	WaitQ	recvq;			// list of recv waiters
	WaitQ	sendq;			// list of send waiters
	Lock;
};

發送流程

Hchan中的兩個WaitQ(recvqsendq)是兩個隊列,分別保存等待從該channel提取和發送的goroutine。以向沒有buffer的channel發送爲例,post

  • 若是向該channel發送數據的goroutine發現recvq不爲空,則從recvq中取出一個goroutine,而後把數據傳給它,發送完成,發送方goroutine能夠繼續執行。提取方goroutine則結束block狀態,能夠被調度執行。
  • 不然,發送方goroutine被存入sendq隊列,且發送方goroutine進入block狀態,調度算法選擇其它goroutine執行。

若是channel有buffer,ui

  • 若是buffer裏有空間,則把數據存入buffer,發送完成;若是recvq隊列裏有等待的goroutine,則取出一個,並將其喚醒,等待調度執行。發送方goroutine繼續執行。
  • 若是buffer已滿,則發送方goroutine被存入sendq隊列,發送方goroutine進入block狀態,調度算法選擇其它goroutine執行。

若是向已經關閉的channel發送數據,程序會報錯並異常退出。以下面的程序:url

package main

func main() {
	c := make(chan int)
	d := make(chan int)
	go func() {
		<-d
		close(c)
	} ()
	d <- 4
	c <- 3
}

從已經關閉的channel收取數據不會報錯,也不會異常退出,可是我不肯定獲得什麼樣的值。除此以外,提取和發送的實現基本是相對的,就再也不介紹了。spa

Buffer空間

buffer的空間緊挨着channel,是在建立的channel的時候一塊兒分配的,code

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

其中hint即爲buffer的元素個數,會保存在dataqsiz裏,另一起管理buffer的還有qcountsendxrecvx,分別表示buffer裏的元素個數,下一次發送操做存放數據的位置,以及下一次提取數據的位置。這個buffer是個circular buffer。blog

Channel與Select

channel配合select語句,能夠實現multiplex的效果,如:

select {
case <-c1:
case <-c2:
}

c1c2哪一個channel先有數據到達,哪一個case先執行;都沒有數據,就block住;都有數據,以一個公平的方式隨機選擇一個case執行。select語句自己沒有增長channel的操做方式,可是它自己的實現也頗有趣:

  • 當select被block住,它所在的goroutine將被掛在多個channel的sendq或者recvq上。好比上面的例子中,select所在的goroutine將被掛在c1c2recvq上,若是這時有另外兩個goroutine同時分別向c1c2發送數據,那麼它們將操做同一個goroutine(儘管是不一樣的channel),這種狀況下,要麼加鎖,要麼用原子操做。這就是爲何dequeue裏要使用runtime·cas的緣由,雖然調用dequeue以前上鎖了,但那是給sendq/recvq上鎖,不是給goroutine上鎖。
  • 不一樣goroutine裏面的select語句可能操做同一組channel,那麼就有上鎖的必要。Go的實現裏每一個channel有本身的鎖,因此select就須要上多個鎖,稍有不慎,可能致使死鎖。Go的實現是用bubble sort把channel的地址(即Hchan*)排序,而後依次上鎖。
  • 最後就是如何實現相對公平。

相對公平的另外一個說法就是每一個channel被選中的機率是相等的。實現以下:

	for(i=0; i<sel->ncase; i++)
		sel->pollorder[i] = i;
	for(i=1; i<sel->ncase; 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來講,是公平的。

 

轉自:http://alpha-blog.wanglianghome.org/2012/04/13/go-channel-implementation/

相關文章
相關標籤/搜索