徹底搞懂 channel 原理 -三-

上一篇文章主要通過一個現實例子間接反映channel的一些原理。最後一篇開始介紹一些細節,會涉及到源碼。

還是從一個簡單的代碼程序看起。

我們創建了一個無緩衝channel,然後往這個channel發送數據。因爲程序中沒有讀操作ready,所以發送的時候會阻塞。我們通過彙編代碼看它底層的調用。

從圖中我們看到,上述發送操作,程序運行時實際調用的runtime.chansend1

最終chansend1最終調用的還是chansendchansend的第三個參數block是個bool值,表示操作channel不能立即成功時是否需要阻塞。

具體哪些操作?

碰到上面的操作,如果不是特殊處理,我們的應用程序會被阻塞,直到被喚醒。

當然對於向nilchannel發送 | 接收數據,後續再也沒機會被喚醒了。

那麼如果是快速試錯的場景,是不是隻要把block改成false,在失敗的場景下就不會被阻塞了。

編譯這段代碼。

可以看出,上面這段代碼編譯後調用selectnbsend最終發送動作調用的還是chansned,只是傳入的blockfalse。這樣一旦操作失敗,程序不會被阻塞。

同理我們可以得出接收的調用動作。

到這裏我們已經知道,

接下來我們來說明這兩個函數底層是如何操作的。

我們還是以一個無緩衝的channel和緩衝channel來說明。

值得一提的是,在使用go func的時候,本質上調用的是runtime.newproc創建一個g,然後把這個g交給調度器調度。

至於什麼時候g被調度,然後執行你的代碼邏輯,那就要看調度器的 "心情" 了。

所以上面創建的兩個g(暫且稱爲 g1 和 g2),可以看成是我們向調度器提交了兩個任務g,我們無法保證哪個g會被先調度器調度執行,因此我們也不確定發送和接收這兩個操作,誰會先被執行。

假設g1先被調度器運行,然後執行代碼ch<-struct{}{}

如果g2先被調度器運行,然後執行代碼<-ch

當然我們也可以把上面的代碼轉化成詳細的無緩衝隊列核心流程圖。

緩衝channel發送的時候分爲三種情況,想想我們上篇文章快遞員送快遞場景。

我們來創建一個例子。

我們創建了一個緩衝區爲 7 的channelbuffer就是用來存儲緩衝元素的,它實際上是一個環形數組。爲什麼是環形的?因爲這樣就可以達到複用空間的效果。

此時沒有發送接收動作,所以qcount爲 0,發送 (sendx) 和接收 (recvx) 的位置都爲 0。

我們來看上面的第一種情況。緩衝區未滿,

這塊代碼就比較簡單了。如果緩衝區未滿,那就把當前要發送的數據拷貝到緩衝區的發送位置,然後發送位置sendx+1,然後當前channel個數qcount+1,整個流程就結束了。

如果緩衝滿的情況下,封裝當前gsudog,把這個sudog入隊等待發送隊列,最後調用gopark掛起當前g,上面無緩衝的時候有提到。

最後一種情況,發送的時候正好有等待接收消息者,那麼就從recvq中拿出最早開始等待的接受者,然後把發送的數據直接拷貝給他。

send整體有兩個動作:拷貝數據 -----> 喚醒等待的recvq

那麼對於接收操作呢?

第一種情況就簡單了。直接通過當前讀位置recvx讀取buffer對應的值,這裏還需要通過判斷是否忽略返回值,而決定需不需要往當前接收操作拷貝數據。然後移動recvx位置,元素個數qcount-- ,最後解鎖即可。

第二種情況,封裝當前gsudog,把這個sudog入隊等待接收隊列,最後調用gopark掛起當前g。上面無緩衝的時候畫過這個邏輯。

第三種情況有點複雜。

這種情況下,當獲取到一個等待發送者,對於接收者來說,如果我們直接拿它的發送數據返回會發生什麼?舉個例子,

上圖,channel滿了,且sendq有一個等待發送者 (假設是G8,發送數據爲800),此時執行接收操作,也就出現上述第三種情況。

如果此時我們直接拿G8的數據,那麼數據就不能保證先入先出了。

所以正確的操作是,讀取當前recvx位置 (0)buffer100,然後把G8的數據 800 拷貝到 0 的位置,最後把recvq的位置向前移動,同步發送位置sendx等於recvq。這裏,可以思考下爲啥?

到這裏緩衝channel 的核心流程就說完了。如圖,


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P0b6a3F-5yDNgnaiQQVAkA