徹底搞懂 channel 原理 -三-
上一篇文章主要通過一個現實例子間接反映channel
的一些原理。最後一篇開始介紹一些細節,會涉及到源碼。
還是從一個簡單的代碼程序看起。
我們創建了一個無緩衝channel
,然後往這個channel
發送數據。因爲程序中沒有讀操作ready
,所以發送的時候會阻塞。我們通過彙編代碼看它底層的調用。
從圖中我們看到,上述發送操作,程序運行時實際調用的runtime.chansend1
。
最終chansend1
最終調用的還是chansend
,chansend
的第三個參數block
是個bool
值,表示操作channel
不能立即成功時是否需要阻塞。
具體哪些操作?
-
向無緩衝
channel
發送數據且當前無接收者ready
。 -
接收無緩衝
channel
數據且當前無發送者ready
。 -
緩衝
channel
已滿,往channel
發送數據。 -
緩衝
channel
爲空,接收channel
數據 -
向一個
nil
的channel
發送數據。(注意,向一個nil
的channel
發送數據並不會引發panic
)。 -
向一個
nil
的channel
接收數據。
碰到上面的操作,如果不是特殊處理,我們的應用程序會被阻塞,直到被喚醒。
當然對於向nil
的channel
發送 | 接收數據,後續再也沒機會被喚醒了。
那麼如果是快速試錯的場景,是不是隻要把block
改成false
,在失敗的場景下就不會被阻塞了。
編譯這段代碼。
可以看出,上面這段代碼編譯後調用selectnbsend
最終發送動作調用的還是chansned
,只是傳入的block
是false
。這樣一旦操作失敗,程序不會被阻塞。
同理我們可以得出接收的調用動作。
-
發送數據,最終調用的
runtime.chansend
。 -
接收數據,最終調用的
runtime.chanrecv
。
接下來我們來說明這兩個函數底層是如何操作的。
我們還是以一個無緩衝的channel
和緩衝channel
來說明。
值得一提的是,在使用go func
的時候,本質上調用的是runtime.newproc
創建一個g
,然後把這個g
交給調度器調度。
至於什麼時候g
被調度,然後執行你的代碼邏輯,那就要看調度器的 "心情" 了。
所以上面創建的兩個g
(暫且稱爲 g1 和 g2),可以看成是我們向調度器提交了兩個任務g
,我們無法保證哪個g
會被先調度器調度執行,因此我們也不確定發送和接收這兩個操作,誰會先被執行。
假設g1
先被調度器運行,然後執行代碼ch<-struct{}{}
。
如果g2
先被調度器運行,然後執行代碼<-ch
。
當然我們也可以把上面的代碼轉化成詳細的無緩衝隊列核心流程圖。
緩衝channel
發送的時候分爲三種情況,想想我們上篇文章快遞員送快遞場景。
-
如果快遞櫃未滿,直接把快遞放入到快遞櫃。(對應緩衝區未滿,把發送數據拷貝到緩衝區)
-
如果快遞櫃滿了,那快遞員只能在那等待快遞櫃空了。(對應把當前
g
封裝成sudog
,然後把 sudog 放到等待發送消息隊列sendq
中,最後掛起當前g
) -
如果送快遞的時候正好客戶在那裏等,那就直接把快遞給他就是了 (對應如果發送的時候發現有等待者,直接數據拷貝給他唄)
我們來創建一個例子。
我們創建了一個緩衝區爲 7 的channel
。buffer
就是用來存儲緩衝元素的,它實際上是一個環形數組。爲什麼是環形的?因爲這樣就可以達到複用空間的效果。
此時沒有發送接收動作,所以qcount
爲 0,發送 (sendx
) 和接收 (recvx
) 的位置都爲 0。
我們來看上面的第一種情況。緩衝區未滿,
這塊代碼就比較簡單了。如果緩衝區未滿,那就把當前要發送的數據拷貝到緩衝區的發送位置,然後發送位置sendx+1
,然後當前channel
個數qcount+1
,整個流程就結束了。
如果緩衝滿的情況下,封裝當前g
成sudog
,把這個sudog
入隊等待發送隊列,最後調用gopark
掛起當前g
,上面無緩衝的時候有提到。
最後一種情況,發送的時候正好有等待接收消息者,那麼就從recvq
中拿出最早開始等待的接受者,然後把發送的數據直接拷貝給他。
send
整體有兩個動作:拷貝數據 -----> 喚醒等待的recvq
。
那麼對於接收操作呢?
-
快遞櫃裏有我的快遞,那我直接拿就行了。(對應緩衝區有數據,根據讀
recvx
的位置拿數據) -
快遞櫃還沒我的快遞,但是快遞哥打電話說快到了,那我現在樓下轉轉。(對應緩衝區無數據,把當前
g
封裝成sudog
, 然後放入到等待接收消息隊列recvq
中)。 -
去拿一個快遞的時候,正好一個快遞員放我另一個快遞的時候因爲快遞櫃滿了,在那等着。(對應緩衝區滿了,且還有等待發送者。此時先到緩衝區獲取當前讀
recvx
位置的數據,然後再從等待發送者隊列中取出最早等待的發送者,把他要發送的數據拷貝拷貝到當前我讀取數據的位置 (保證先入先出的順序),最後更新發送位置和更新位置即可)。
第一種情況就簡單了。直接通過當前讀位置recvx
讀取buffer
對應的值,這裏還需要通過判斷是否忽略返回值,而決定需不需要往當前接收操作拷貝數據。然後移動recvx
位置,元素個數qcount--
,最後解鎖即可。
第二種情況,封裝當前g
成sudog
,把這個sudog
入隊等待接收隊列,最後調用gopark
掛起當前g
。上面無緩衝的時候畫過這個邏輯。
第三種情況有點複雜。
這種情況下,當獲取到一個等待發送者,對於接收者來說,如果我們直接拿它的發送數據返回會發生什麼?舉個例子,
上圖,channel
滿了,且sendq
有一個等待發送者 (假設是G8
,發送數據爲800
),此時執行接收操作,也就出現上述第三種情況。
如果此時我們直接拿G8
的數據,那麼數據就不能保證先入先出了。
所以正確的操作是,讀取當前recvx
位置 (0)buffer
值100
,然後把G8
的數據 800 拷貝到 0 的位置,最後把recvq
的位置向前移動,同步發送位置sendx
等於recvq
。這裏,可以思考下爲啥?
到這裏緩衝channe
l 的核心流程就說完了。如圖,
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/P0b6a3F-5yDNgnaiQQVAkA