Go語言 | 基於channel實現的並發安全的字節池

2021-02-16 GoCN

字節切片[]byte是我們在編碼中經常使用到的,比如要讀取文件的內容,或者從io.Reader獲取數據等,都需要[]byte做緩衝。

func ReadFull(r Reader, buf []byte) (n int, err error)
func (f *File) Read(b []byte) (n int, err error)

以上是兩個使用到[]byte作為緩衝區的方法。那麼現在問題來了,如果對於以上方法我們有大量的調用,那麼就要聲明很多個[]byte,這需要太多的內存的申請和釋放,也就會有太多的GC。

MinIO 的字節池

這個時候,我們需要重用已經創建好的[]byte來提高對象的使用率,降低內存的申請和GC。這時候我們可以使用sync.Pool來實現,不過最近我在研究開源項目MinIO的時候,發現他們使用channel的方式實現字節池。

type BytePoolCap struct {
    c    chan []byte
    w    int
    wcap int
}

BytePoolCap結構體的定義比較簡單,共有三個欄位:

c是一個chan,用於充當字節緩存池

w是指使用make函數創建[]byte時候的len參數

wcap指使用make函數創建[]byte時候的cap參數

有了BytePoolCap結構體,就可以為其定義Get方法,用於獲取一個緩存的[]byte了。

func (bp *BytePoolCap) Get() (b []byte) {
    select {
    case b = <-bp.c:
    // reuse existing buffer
    default:
        // create new buffer
        if bp.wcap > 0 {
            b = make([]byte, bp.w, bp.wcap)
        } else {
            b = make([]byte, bp.w)
        }
    }
    return
}

以上是採用經典的select+chan的方式,能獲取到[]byte緩存則獲取,獲取不到就執行default分支,使用make函數生成一個[]byte。

從這裡也可以看到,結構體中定義的w和wcap欄位,用於make函數的len和cap參數。

有了Get方法,還要有Put方法,這樣就可以把使用過的[]byte放回字節池,便於重用。

func (bp *BytePoolCap) Put(b []byte) {
    select {
    case bp.c <- b:
        // buffer went back into pool
    default:
        // buffer didn't go back into pool, just discard
    }
}

Put方法也是採用select+chan,能放則放,不能放就丟棄這個[]byte。

使用BytePoolCap

已經定義好了Get和Put就可以使用了,在使用前,BytePoolCap還定義了一個工廠函數,用於生成*BytePoolCap,比較方便。

func NewBytePoolCap(maxSize int, width int, capwidth int) (bp *BytePoolCap) {
    return &BytePoolCap{
        c:    make(chan []byte, maxSize),
        w:    width,
        wcap: capwidth,
    }
}

把相關的參數暴露出去,可以讓調用者自己定製。這裡的maxSize表示要創建的chan有多大,也就是字節池的大小,最大存放數量。

bp := bpool.NewBytePoolCap(500, 1024, 1024)
buf:=bp.Get()
defer bp.Put(buf)

//使用buf,不再舉例

以上就是使用字節池的一般套路,使用後記得放回以便復用。

和sync.Pool對比

兩者原理基本上差不多,都多協程安全。sync.Pool可以存放任何對象,BytePoolCap只能存放[]byte,不過也正因為其自定義,存放的對象類型明確,不用經過一層類型斷言轉換,同時也可以自己定製對象池的大小等。

關於二者的性能,我做了下Benchmark測試,整體看MinIO的BytePoolCap更好一些。

var bp = bpool.NewBytePoolCap(500, 1024, 1024)
var sp = &sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024, 1024)
    },
}

模擬的兩個字節池,[]byte的長度和容量都是1024。然後是兩個模擬使用字節池,這裡我啟動500協程,模擬並發,使用不模擬並發的話,BytePoolCap完全是一個[]byte的分配,完全秒殺sync.Pool,對sync.Pool不公平。

func opBytePool(bp *bpool.BytePoolCap) {
    var wg sync.WaitGroup
    wg.Add(500)
    for i := 0; i < 500; i++ {
        go func(bp *bpool.BytePoolCap) {
            buffer := bp.Get()
            defer bp.Put(buffer)
            mockReadFile(buffer)
            wg.Done()
        }(bp)
    }
    wg.Wait()
}

func opSyncPool(sp *sync.Pool) {
    var wg sync.WaitGroup
    wg.Add(500)
    for i := 0; i < 500; i++ {
        go func(sp *sync.Pool) {
            buffer := sp.Get().([]byte)
            defer sp.Put(buffer)
            mockReadFile(buffer)
            wg.Done()
        }(sp)
    }
    wg.Wait()
}

接下來就是我模擬的讀取我本機文件的一個函數mockReadFile(buffer):

func mockReadFile(b []byte) {
    f, _ := os.Open("water")
    for {
        n, err := io.ReadFull(f, b)
        if n == 0 || err == io.EOF {
            break
        }
    }
}

然後運行go test -bench=. -benchmem -run=none 查看測試結果:

pkg: flysnow.org/hello
BenchmarkBytePool-8         1489            979113 ns/op           36504 B/op       1152 allocs/op
BenchmarkSyncPool-8         1008           1172429 ns/op           57788 B/op       1744 allocs/op

從測試結果看BytePoolCap在內存分配,每次操作分配字節,每次操作耗時來看,都比sync.Pool更有優勢。

小結

很多優秀的開源項目,可以看到很多優秀的原始碼實現,並且會根據自己的業務場景,做出更好的優化。

本文為原創文章,轉載註明出處,歡迎掃碼關注公眾號flysnow_org或者網站asf https://www.flysnow.org/ ,第一時間看後續精彩文章。覺得好的話,請猛擊文章右下角「在看」,感謝支持。

相關焦點

  • Go 語言基於 channel 實現的並發安全的字節池
    MinIO 的字節池這個時候,我們需要重用已經創建好的[]byte來提高對象的使用率,降低內存的申請和GC。這時候我們可以使用sync.Pool來實現,不過最近我在研究開源項目MinIO的時候,發現他們使用channel的方式實現字節池。
  • 知乎問答:為什麼字節跳動選擇使用 Go 語言?
    py 寫起來效率極高,不過對我們業務而言,有幾個敏感的問題,GIL 對多核不友好,高並發和 cpu 密集情況下必須得從架構層面去設計了,另外考慮健壯性,動態語言做不到高覆蓋率或有較強 code review 把控的話,業務極易有疏漏的地方,當然通過 try catch 和線上函數替換實現熱修能緩解一些焦慮,但總歸不是能體現在研發流程中的常規路數。
  • Golang並發:再也不愁選channel還是選鎖
    如果自己心裡還沒有清晰的答案,那就讀下這篇文章,你會了解到:前戲前面很多篇的文章都在圍繞channel介紹,而只有前一篇sync的文章介紹到了Mutex,不是我偏心,而是channel在Golang是first class級別的,設計在語言特性中的,而Mutex只是一個包中的。這就註定了一個是主角,一個是配角。
  • 一文帶你解密 Go 語言之通道 channel
    接下來和煎魚一起正式開始 Go channel 的學習之旅!Go 語言中的一大利器那就是能夠非常方便的使用 go 關鍵字來進行各種並發,而並發後又必然會涉及通信。channel 基本特性在 Go 語言中,channel 的關鍵字為 chan,數據流向的表現方式為 <-,代碼解釋方向是從左到右,據此就能明白通道的數據流轉方向了。
  • Go語言和Java、Python等其他語言的對比分析
    可以藉助通道實現 goroutines 之間的通信。Goroutines 以及基於通道的並發性方法使其非常容易使用所有可用的 CPU 內核,並處理並發的 IO。相較於 Python/Java,在一個 goroutine 上運行一個函數需要最小的代碼。
  • 從bug中學習:六大開源項目告訴你go並發編程的那些坑
    導語 | 並發編程中,go不僅僅支持傳統的通過共享內存的方式來通信,更推崇通過channel來傳遞消息,這種新的並發編程模型會出現不同於以往的bug。
  • 60分鐘快速了解Go語言
    發明Go語言是出於更好地完成工作的需要。Go不是計算機科學的最新發展潮流,但它卻提供了解決現實問題的最新最快的方法。Go擁有命令式語言的靜態類型,編譯很快,執行也很快,同時加入了對於目前多核CPU的並發計算支持,也有相應的特性來實現大規模編程。Go語言有非常棒的標準庫,還有一個充滿熱情的社區。
  • [GO語言基礎] 一.為什麼我要學習Golang以及GO語言入門普及
    Go 語言語法與 C 相近,但功能上有:內存安全,GC(垃圾回收),結構形態及 CSP-style 並發計算。該語言的吉祥物為金花鼠(gordon),如下圖所示。下面我參考知乎和網上大神的答案,談談GO語言的優勢。Go 語言特色簡潔、快速、安全並行、有趣、開源內存管理、數組安全、編譯迅速Go 語言用途Go 語言被設計成一門應用於搭載 Web 伺服器,存儲集群或類似用途的巨型中央伺服器的系統程式語言。對於高性能分布式系統領域而言,Go 語言無疑比大多數其它語言有著更高的開發效率。
  • Go語言聖經-學習筆記01
    不同的有:interface、select、chan、package、defer、gochan  :(channel通道 )並發通道defer:對函數進行析構go:並發相關interface:接口聲明關鍵字2.2聲明四種類型:var變量、const常量、type類型、func函數var變量
  • Go語言愛好者周刊:第 82 期 — 情人節快樂
    2、圖解 Go Select 語句的執行順序select 允許在一個 goroutine 中管理多個 channel。但是,當所有 channel 同時就緒的時候,go 需要在其中選擇一個執行。此外,go 還需要處理沒有 channel 就緒的情況,我們先從就緒的 channel 開始。3、Go:符號表是什麼?如何利用符號表是由編譯器生成和維護的,保存了與程序相關的信息,如函數和全局變量。理解符號表能幫助我們更好地與之交互和利用它。
  • Golang 入門筆記 - Channel
    via:https://medium.com/technofunnel/understanding-goroutine-go-channels-in-detail-9c5a28f08e0d作者:Mayank Gupta 四哥水平有限,如有翻譯或理解錯誤,煩請幫忙指出,感謝!這篇文章源自 Medium,文章點讚 600+。
  • go語言有哪些優勢
    而且從Go語言的發展態勢來看,Google對它這個新的寵兒還是很看重的,Go自然有一個良好的發展前途。4、自由高效:組合的思想、無侵入式的接口Go語言可以說是開發效率和運行效率二者的完美融合,天生的並發編程支持。Go語言支持當前所有的編程範式,包括過程式編程、面向對象編程、面向接口編程、函數式編程。
  • 「GCTT 出品」對 Go 中長時間運行 io.Reader 和 io.Writer 的操作...
    封裝 Reader一個新的 Reader 類型只需要包含另一個 io.Reader , 並且調用它的 Read 方法來獲取返回前讀到的字節數。為了保證 reader 可以在並發環境中安全使用(在這個例子中至關重要),我們可以使用 atomic.AddInt64 作為安全的計數器。
  • 乾貨 | Go/Python/Erlang程式語言對比分析及示例
    Go的很多語言特性借鑑與它的三個祖先:C,Pascal和CSP。Go的語法、數據類型、控制流等繼承於C,Go的包、面對對象等思想來源於Pascal分支, 而Go最大的語言特色,基於管道通信的協程並發模型,則借鑑於CSP分支。
  • Go語言愛好者周刊:第 80 期 — 認真思考下為什麼?
    1、字節跳動教育業務研發崗太缺人了吧!!來自 Go 語言中文網微信群裡的問題。3、知乎問答:為什麼字節跳動選擇使用 Go 語言?一鳴不喜歡 Java 亮了。7、Go 並發編程-信號量的使用方法和其實現原理信號量是並發編程中常見的一種同步機制,在需要控制訪問資源的線程數量時就會用到信號量8、Golang unsafe.Pointer 使用原則以及 uintptr 隱藏的坑
  • 一文搞定Go語言語法
    對他們來說,這是一種令人興奮的體驗,因為他們看到伺服器可以處理的並發請求數量大幅增加。當您使用非並發(Node.js)或全局解釋器鎖定的解釋型語言時,這實際上是相當正常的。結合語言的簡易性,這解釋了 Go 令人興奮的原因。然而與 Java 相比,在原始性能基準測試中,情況並不是那麼清晰。
  • 「GCTT 出品」關於結構化並發的筆記——Go 語言中有害的聲明語句
    並發程序的編寫和推理是非常困難的。基於goto的程序也是如此。這可能是出於某些相同的原因嗎?在現代語言中,goto引起的問題在很大程度上得到解決。如果我們從 」研究他們如何修正「 轉到 」它會教我們如何製作更多可用的並發API「,讓我們來找出答案。goto發生了什麼事?
  • GO語言入門(第一個go程序)
    語言的特性Go 語言從本質上(程序和結構方面)來實現並發編程。因為 Go 語言沒有類和繼承的概念,所以它和 Java 或 C++ 看起來並不相同。但是它通過接口(interface)的概念來實現多態性。
  • 字節跳動面試官:請用JS實現Ajax並發請求控制
    今天這道是字節跳動的:實現一個批量請求函數 multiRequest(urls, maxNum),要求如下:• 要求最大並發數 maxNum• 每當有一個請求返回,就留下一個空位,可以增加新的請求• 所有請求完成後,結果按照 urls 裡面的順序依次打出
  • 一篇文章帶你入門Go語言基礎之並發
    前言Hey,大家好,我是碼農星期八,終於到了Go中最牛掰的地方,並發,這也是Go為什麼能快速火的原因。引言Go語言,專門為並發而生的語言,每啟動一個微線程創建一個代價大概2KB起步假設一個內存條大小4G,一個微線程2kb,1G=1024M=1048576kb,1048576/2=524288,五十多萬個但是你知道像Java,Python等語言,一個線程代價多大嗎???