無限緩衝的 channel -1-

圖片拍攝於 2021 年 5 月 20 日,公司活動日。

開篇介紹

事情的起因是前幾周看到鳥窩寫了一篇關於實現無限緩衝 channel 的文章,當時忙着和小姐姐聊天沒看,今天想起來了。

不過這篇文章不會涉及到鳥窩自己實現的 chanx,我們會在下一篇提到。

我們都知道,channel 有兩種類型: 無緩衝和有緩衝的。

當我們創建一個有緩衝的通道並指定了容量,那麼在這個通道的生命週期內,我們將再也無法改變它的容量。

有時候,我們並不知道也無法預估寫入通道的數量規模。如果此時通道的寫入速度遠遠超過讀取速度,那麼必然會在某個時間點塞滿通道,導致寫入阻塞。 

比如之前我翻譯的一篇文章 使用 Go 每分鐘處理百萬請求 中,作者就出現處理速度太慢,導致通道塞滿,其他請求被阻塞,響應時間慢慢增加。

此時有人就會提到,能不能提供一個無限緩衝 (Unbounded or Unlimited) 的通道。

這個問題早在 2017 年就有人提過 issues,最終 go 官方沒有實現這個提案。

不過,這個 issues 下面總共產生了 67 個 comments,評論很精彩。 

比如有人提到:

cznic:Unlimited capacity channels ask for a machine with unlimited memory.

那麼如何實現一個無限緩衝的通道呢?

針對這類需求,有很多版本的實現,我們來看其中的一個實現。鳥窩的 chanx 就是在這個基礎上做修改的。

我們一步步還原它的實現,這其中還能知道作者的思考過程。

代碼實現

第一版,

MakeInfinite 函數返回兩個通道,第一個用於數據的寫入,第二個用於數據的讀取。

注意看這裏的細節,在返回的時候就約束了通道的操作類型: 一個只寫,一個只讀,這樣避免了用戶破壞通道的操作流程。 這裏面的代碼也簡單,只要寫入通道 in 未被關閉,那麼就把從 in 通道中讀取的值 append 到 inQueue 切片中。 inQueue 在這裏就是實現無限緩衝的中間層。

然後有個 test。

當走到第二個 case 的時候,由於 inQueue 一開始是空的,那麼必然會出現 index out。 不僅是一開始,在運行中,如果讀取比寫入快,那麼必然也會導致相同的情況。

在 inQueue 沒有值的時候,我們把 nil 也寫入到通道, 然後測試代碼中我們從 out channel 讀取值試圖把值斷言 int 失敗了。 那麼,當隊列中沒有數據時,我們不應該寫入 out 通道。

 作者使用了一個技巧,如果 inQueue 沒有數據,那麼嘗試寫入一個 nil 通道將永遠阻塞。 通常,永久阻塞是一個不好的行爲,但是這個是包含在 select 語句中的,所以問題不大。

還有問題。原因很簡單,我們再發送完數據就馬上關閉了 in 通道。隨後 break loop。接下來關閉 out 通道,程序運行結束。 此時 inQueue 還有值未被取出。

只要寫比讀快,那麼就永遠存在這個問題。我們需要保證在通道關閉的時候,inQueue 已爲空。 

總結

上面是如何實現一個無限緩衝的 channel

答: 藉助了一個臨時存儲數據的中間層。

上面的實現有沒有哪些地方可以改進?

inQueue 作爲中間層,本質上是一個切片。如果 inQueue 已經擴容到很大的值了,但是並沒有對應的 reset。會導致 inQueue 指向還在底層數組靠後的位置,並不能複用數組前面的空間,造成浪費。

chanx 是咋麼改進的?

下一篇。

參考

https://github.com/golang/go/issues/20352

https://colobu.com/2021/05/11/unbounded-channel-in-go

https://medium.com/capital-one-tech/building-an-unbounded-channel-in-go-789e175cd2cd

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