廢話很少說,直奔主題。git
簡單說明:github
buf
是有緩衝的channel所特有的結構,用來存儲緩存數據。是個循環鏈表sendx
和recvx
用於記錄buf
這個循環鏈表中的~發送或者接收的~indexlock
是個互斥鎖。recvq
和sendq
分別是接收(<-channel)或者發送(channel <- xxx)的goroutine抽象出來的結構體(sudog)的隊列。是個雙向鏈表源碼位於/runtime/chan.go
中(目前版本:1.11)。結構體爲hchan
。golang
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。bash
ch := make(chan int, 3)
複製代碼
建立channel實際上就是在內存中實例化了一個hchan
的結構體,並返回一個ch指針,咱們使用過程當中channel在函數之間的傳遞都是用的這個指針,這就是爲何函數傳遞中無需使用channel的指針,而直接用channel就好了,由於channel自己就是一個指針。微信
先考慮一個問題,若是你想讓goroutine以先進先出(FIFO)的方式進入一個結構體中,你會怎麼操做? 加鎖!對的!channel就是用了一個鎖。hchan自己包含一個互斥鎖mutex
函數
channel中有個緩存buf,是用來緩存數據的(假如實例化了帶緩存的channel的話)隊列。咱們先來看看是如何實現「隊列」的。 仍是剛纔建立的那個channelui
ch := make(chan int, 3)
複製代碼
當使用send (ch <- xx)
或者recv ( <-ch)
的時候,首先要鎖住hchan
這個結構體。this
而後開始send (ch <- xx)
數據。 一spa
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的原則。
注意:緩存鏈表中以上每一步的操做,都是須要加鎖操做的!
每一步的操做的細節能夠細化爲:
每一步的操做總結爲動態圖爲:(發送過程)
或者爲:(接收過程)
因此不難看出,Go中那句經典的話:Do not communicate by sharing memory; instead, share memory by communicating.
的具體實現就是利用channel把數據從一端copy到了另外一端! 還真是符合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隊列中。
你可能會順着以上的思路反推。首先:
這個時候G2會主動調用Go的調度器,讓G2等待,並從讓出M,讓其餘G去使用。 G2還會被抽象成含有G2指針和recv空元素的sudog
結構體保存到hchan的recvq
中等待被喚醒
此時剛好有個goroutine G1開始向channel中推送數據 ch <- 1
。 此時,很是有意思的事情發生了:
G1並無鎖住channel,而後將數據放到緩存中,而是直接把數據從G1直接copy到了G2的棧中。 這種方式很是的贊!在喚醒過程當中,G2無需再得到channel的鎖,而後從緩存中取數據。減小了內存的copy,提升了效率。
以後的事情顯而易見:
互聯網技術窩
或者加微信共同探討交流:參考文獻: