channel in Go's runtime

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

Go語言有一個非常大的亮點就是支援語言層級的並發。語言層級提供並發編程,究竟有多重要,可能需要你親自去體會多線程、事件+callback等常見的並發並發編程模型後才能準確的感受到。為了配合語言層級的並發支援,channel組件就是Go語言必不可少的了。官方提倡的一個編程信條——“使用通訊去共用記憶體,而不是共用記憶體去通訊”,這裡說的”通訊去共用記憶體”的手段就是channel。

channel的實現位於runtime/chan.c檔案中。

channel底層結構模型

每個channel都是由一個Hchan結構定義的,這個結構中有兩個非常關鍵的欄位就是recvq和sendq。recvq和sendq是兩個等待隊列,這個兩個隊列裡分別儲存的是等待在channel上進行讀操作的goroutine和等待在channel上進行寫操作的goroutine。

當我們使用make()建立一個channel後,這個channel的大概記憶體模型就如,有一個Hchan結構頭部,頭部後面的所有記憶體將被劃分為一個一個的slot,每個slot將儲存一個元素。slot的個數當然就是make channel時指定的緩衝大小。如果make的channel是無緩衝的,那麼這裡就沒有slot了,就只有Hchan這個頭部結構。channel的這個底層實現就是分配的一段連續記憶體(數組),不是採用的鏈表或者其他的什麼進階資料結構,事實上做這件事情也不需要進階的資料結構了。

這裡的所有slot形成的數組本身在不移動記憶體的情況下,是無法做到FIFO的,事實上,Hchan中還有兩個關鍵字段recvx和sendx,在它們的配合下就將slot數組構成了一個迴圈數組,就這樣利用數組實現了一個迴圈隊列。

這裡得吐槽一小段代碼,這段代碼就是在make一個channel的函數中。

#defineMAXALIGN7Hcan *c;// calculate rounded size of Hchann = sizeof(*c);while(n & MAXALIGN)n++;

這裡的while迴圈就是要將Hchan結構的大小向上補齊到8的倍數,這樣後面的記憶體空間就是按8位元組對齊了。為了完成這個向上的補齊操作,最壞情況要執行7次迴圈,而事實上是可以一步到位的補齊到8的倍數,完全沒必要一次一次的加1進行嘗試。這個細節其實在很多代碼裡都有,Nginx就做得很優雅。我是想說Go的部分代碼還是挺奔放的,我個人很不喜歡runtime裡面的一些函數/變數的命名。

寫channel

有了channel的底層結構模型,基本上也能想象一個元素是如何在channel進行”入隊/出隊”了。完成寫channel操作的函數是runtime·chansend,這個函數同時實現了同步/非同步寫channel,也就是帶/不帶緩衝的channel的寫操作都是在這個函數裡實現的。同步寫,還是非同步寫,其實就是判斷是否有slot。這裡敘述一下寫channel的過程,不再展示代碼了。

  1. 加鎖,鎖住整個channel結構(就是上面的貼圖模型)。加鎖是可以理解,只是這個鎖也夠大的。所以,是否一定總是通過“通訊來共用記憶體”是需要謹慎考慮的。這把鎖可以看出,channel很多時候不一定有直接對共用變數加鎖效率高。
  2. 現在已經鎖住了整個channel了,可以開始幹活了。判斷是否有slot(是否帶緩衝),如果有就做非同步寫,沒有就做同步寫。
  3. 假設第2步判斷的是同步寫,那麼就試著從recvq等待隊列裡取出一個等待的goroutine,然後將要寫入的元素直接交給(拷貝)這個goroutine,然後再將這個拿到元素的goroutine給設定為ready狀態,就可以開始運行了。到這裡並沒有完,如果recvq裡,並沒有一個等待的goroutine,那麼就將待寫入的元素儲存在當前執行寫的goroutine的結構裡,然後將當前goroutine入隊到sendq中並被掛起,等待有人來讀取元素後才會被喚醒。這個時候,同步寫的過程就真的完成了。
  4. 假設第2步判斷的是非同步寫,非同步寫相對同步寫來說,依賴的對象不再是是否有goroutine在等待讀,而是緩衝區是否被寫滿(是否還有slot)。因此,非同步寫的過程和同步寫大體上也是一樣的。首先是判斷是否還有slot可用,如果沒有slot可用了,就將當前goroutine入隊到sendq中並被掛起等待。如果有slot可用,就將元素追加到一個slot中,再從recvq中試著取出一個等待的goroutine開始進行讀操作(如果recvq中有等待讀的goroutine的話)。到這裡,非同步寫也就完成了。

非同步寫和同步寫在邏輯過程上基本是相同的,只是依賴的對象不一樣而已。同步寫依賴是否有等待讀的goroutine,非同步寫依賴是否有可用的緩衝區。

讀channel

我們知道了寫過程的邏輯,試著推測一下讀過程其實一點也不難了。有了寫,本質上就有了讀了。完成讀channel操作的函數是runtime·chanrecv, 下面簡單的敘述一下讀過程。

  1. 同樣首先加鎖,鎖住整個channel好乾活。
  2. a通過是否帶緩衝來判斷做同步讀還是非同步讀, 類似寫過程。
  3. 假設是同步讀,就試著從sendq隊列取出一個等待寫的goroutine,並把需要寫入的元素拿過來(拷貝),再將取出的goroutine給ready起來。如果sendq中沒有等待寫的goroutine,就只能把當前讀的goroutine給入隊到recvq並被掛起了。
  4. 假設是非同步讀,這個時候就是判斷緩衝區中是否有一個元素,沒的話,就是將當前讀goroutine給入隊到recvq並被掛起等待。如果有元素的話,當然就是取出最前面的元素,同時試著從sendq中取出一個等待寫的goroutine喚醒它。

通過讀寫過程可以看出,讀和寫是心心相惜的,裡面有一個非常重要的細節——讀需要去”喚醒”寫的goroutine,寫的時候需要去“喚醒”讀的goroutine。所以這裡的讀寫過程其實是成對出現,配合完成工作的,缺少一個都不行。(我好像在說廢話)

無限大channel的實現

有同事提到如何?一個不限制緩衝區大小的channel,同時還支援select操作。select的實現,放下一次討論了。不管用什麼語言,要實現一個無限制大小的channel,應該都不難。在目前channel的基礎如何?一個無限制大小的channel,在這裡我大概說一下我的想法,拋磚引玉。

現在的channel其實就一個數組而已,為了避免記憶體拷貝,可以在目前的基礎上加一層鏈表結構。這樣一來,只要緩衝區用完後,就可以分配一個新的slot數組,並且和老的數組給鏈起來構成一個更大的緩衝區。這裡代碼上最複雜的應該是元素被讀走後,需要將空的數組給釋放掉。加入鏈表來構造無限制的channel實現看上去是一種比較簡單有效方案。

如果channel是無限制緩衝大小的,那麼寫入的goroutine就永遠不會被掛起等待了,也就不要sendq隊列了。當然,沒消費者或者消費者掛掉的話,這個channel最終也會導致記憶體爆掉。所以,無限制大小的channel是否真的有必要???

瞭解了channel的底層實現,應該可以更好選擇“通訊去共用記憶體,還是共用記憶體去通訊”,沒有什麼是銀彈。

註:本文是基於go1.1.2版本代碼。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.