廢話不多說,直奔主題。
channel的整體結構圖
簡單說明:
buf
是有緩衝的channel所特有的結構,用來儲存快取資料。是個迴圈連結串列sendx
和recvx
用於記錄buf
這個迴圈連結串列中的~傳送或者接收的~indexlock
是個互斥鎖。recvq
和sendq
分別是接收(<-channel)或者傳送(channel <- xxx)的goroutine抽象出來的結構體(sudog)的佇列。是個雙向連結串列
原始碼位於/runtime/chan.go
中(目前版本:1.11)。結構體為hchan
。
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
複製程式碼
下面我們來詳細介紹hchan
中各部分是如何使用的。
先從建立開始
我們首先建立一個channel。
ch := make(chan int, 3)
複製程式碼
建立channel實際上就是在記憶體中例項化了一個hchan
的結構體,並返回一個ch指標,我們使用過程中channel在函式之間的傳遞都是用的這個指標,這就是為什麼函式傳遞中無需使用channel的指標,而直接用channel就行了,因為channel本身就是一個指標。
channel中傳送send(ch <- xxx)和recv(<- ch)接收
先考慮一個問題,如果你想讓goroutine以先進先出(FIFO)的方式進入一個結構體中,你會怎麼操作?
加鎖!對的!channel就是用了一個鎖。hchan本身包含一個互斥鎖mutex
channel中佇列是如何實現的
channel中有個快取buf,是用來快取資料的(假如例項化了帶快取的channel的話)佇列。我們先來看看是如何實現“佇列”的。 還是剛才建立的那個channel
ch := make(chan int, 3)
複製程式碼
當使用send (ch <- xx)
或者recv ( <-ch)
的時候,首先要鎖住hchan
這個結構體。
然後開始send (ch <- xx)
資料。
一
ch <- 1
複製程式碼
二
ch <- 1
複製程式碼
三
ch <- 1
複製程式碼
這時候滿了,佇列塞不進去了 動態圖表示為:
然後是取recv ( <-ch)
的過程,是個逆向的操作,也是需要加鎖。
然後開始recv (<-ch)
資料。
一
<-ch
複製程式碼
二
<-ch
複製程式碼
三
<-ch
複製程式碼
圖為:
注意以上兩幅圖中buf
和recvx
以及sendx
的變化,recvx
和sendx
是根據迴圈連結串列buf
的變動而改變的。
至於為什麼channel會使用迴圈連結串列作為快取結構,我個人認為是在快取列表在動態的send
和recv
過程中,定位當前send
或者recvx
的位置、選擇send
的和recvx
的位置比較方便吧,只要順著連結串列順序一直旋轉操作就好。
快取中按連結串列順序存放,取資料的時候按連結串列順序讀取,符合FIFO的原則。
send/recv的細化操作
注意:快取連結串列中以上每一步的操作,都是需要加鎖操作的!
每一步的操作的細節可以細化為:
- 第一,加鎖
- 第二,把資料從goroutine中copy到“佇列”中(或者從佇列中copy到goroutine中)。
- 第三,釋放鎖
每一步的操作總結為動態圖為:(傳送過程)
或者為:(接收過程)
所以不難看出,Go中那句經典的話:Do not communicate by sharing memory; instead, share memory by communicating.
的具體實現就是利用channel把資料從一端copy到了另一端!
還真是符合channel
的英文含義:
當channel快取滿了之後會發生什麼?這其中的原理是怎樣的?
使用的時候,我們都知道,當channel快取滿了,或者沒有快取的時候,我們繼續send(ch <- xxx)或者recv(<- ch)會阻塞當前goroutine,但是,是如何實現的呢?
我們知道,Go的goroutine是使用者態的執行緒(user-space threads
),使用者態的執行緒是需要自己去排程的,Go有執行時的scheduler去幫我們完成排程這件事情。關於Go的排程模型GMP模型我在此不做贅述,如果不瞭解,可以看我另一篇文章(Go排程原理)
goroutine的阻塞操作,實際上是呼叫send (ch <- xx)
或者recv ( <-ch)
的時候主動觸發的,具體請看以下內容:
//goroutine1 中,記做G1
ch := make(chan int, 3)
ch <- 1
ch <- 1
ch <- 1
複製程式碼
這個時候G1正在正常執行,當再次進行send操作(ch<-1)的時候,會主動呼叫Go的排程器,讓G1等待,並從讓出M,讓其他G去使用
同時G1也會被抽象成含有G1指標和send元素的sudog
結構體儲存到hchan的sendq
中等待被喚醒。
那麼,G1什麼時候被喚醒呢?這個時候G2隆重登場。
G2執行了recv操作p := <-ch
,於是會發生以下的操作:
G2從快取佇列中取出資料,channel會將等待佇列中的G1推出,將G1當時send的資料推到快取中,然後呼叫Go的scheduler,喚醒G1,並把G1放到可執行的Goroutine佇列中。
假如是先進行執行recv操作的G2會怎麼樣?
你可能會順著以上的思路反推。首先:
這個時候G2會主動呼叫Go的排程器,讓G2等待,並從讓出M,讓其他G去使用。
G2還會被抽象成含有G2指標和recv空元素的sudog
結構體儲存到hchan的recvq
中等待被喚醒
此時恰好有個goroutine G1開始向channel中推送資料 ch <- 1
。
此時,非常有意思的事情發生了:
G1並沒有鎖住channel,然後將資料放到快取中,而是直接把資料從G1直接copy到了G2的棧中。 這種方式非常的贊!在喚醒過程中,G2無需再獲得channel的鎖,然後從快取中取資料。減少了記憶體的copy,提高了效率。
之後的事情顯而易見:
更多精彩內容,請關注我的微信公眾號 網際網路技術窩
或者加微信共同探討交流:
參考文獻: