徹底搞懂 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。這裏,可以思考下爲啥?
到這裏緩衝channel 的核心流程就說完了。如圖,
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/P0b6a3F-5yDNgnaiQQVAkA