Go - Pool: 性能提升大殺器

特性

sync.Pool 數(shù)據(jù)類型用來保存一組可獨(dú)立訪問的"臨時"對象,它說明了 sync.Pool 這個數(shù)據(jù)類型的特點(diǎn),也就是說,它池化的對象會在未來的某個時候被毫無預(yù)兆地移除掉。而且,如果沒有別的對象引用這個被移除的對象的話,這個被移除的對象就會被垃圾回收掉。

有兩個知識點(diǎn)需要記?。?/p>

  1. sync.Pool 本身就是線程安全的,多個 goroutine 可以并發(fā)地調(diào)用它的方法存取對象;
  2. sync.Pool 不可在使用之后再復(fù)制使用。

使用方法

只提供了三個對外的方法:New、Get 和 Put

New

Pool struct 包含一個 New 字段,這個字段的類型是函數(shù) func() interface{}。當(dāng)調(diào)用 Pool 的 Get 方法從池中獲取元素,沒有更多的空閑元素可返回時,就會調(diào)用這個 New 方法來創(chuàng)建新的元素。如果你沒有設(shè)置 New 字段,沒有更多的空閑元素可返回時,Get 方法將返回 nil,表明當(dāng)前沒有可用的元素。

Get

如果調(diào)用這個方法,就會從 Pool取走一個元素,這也就意味著,這個元素會從 Pool 中移除,返回給調(diào)用者。不過,除了返回值是正常實(shí)例化的元素,Get 方法的返回值還可能會是一個 nil(Pool.New 字段沒有設(shè)置,又沒有空閑元素可以返回),所以你在使用的時候,可能需要判斷。

func (p *Pool) Get() interface{} {
    // 把當(dāng)前goroutine固定在當(dāng)前的P上
    l, pid := p.pin()
    x := l.private // 優(yōu)先從local的private字段取,快速
    l.private = nil
    if x == nil {
        // 從當(dāng)前的local.shared彈出一個,注意是從head讀取并移除
        x, _ = l.shared.popHead()
        if x == nil { // 如果沒有,則去偷一個
            x = p.getSlow(pid) 
        }
    }
    runtime_procUnpin()
    // 如果沒有獲取到,嘗試使用New函數(shù)生成一個新的
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}

首先,從本地的 private 字段中獲取可用元素,因?yàn)闆]有鎖,獲取元素的過程會非常快,如果沒有獲取到,就嘗試從本地的 shared 獲取一個,如果還沒有,會使用 getSlow 方法去其它的 shared 中“偷”一個。最后,如果沒有獲取到,就嘗試使用 New 函數(shù)創(chuàng)建一個新的。

這里的重點(diǎn)是 getSlow 方法,我們來分析下??疵忠簿椭懒耍暮臅r可能比較長。它首先要遍歷所有的 local,嘗試從它們的 shared 彈出一個元素。如果還沒找到一個,那么,就開始對 victim 下手了。

在 vintim 中查詢可用元素的邏輯還是一樣的,先從對應(yīng)的 victim 的 private 查找,如果查不到,就再從其它 victim 的 shared 中查找。

下面的代碼是 getSlow 方法的主要邏輯:

func (p *Pool) getSlow(pid int) interface{} {

    size := atomic.LoadUintptr(&p.localSize)
    locals := p.local                       
    // 從其它proc中嘗試偷取一個元素
    for i := 0; i < int(size); i++ {
        l := indexLocal(locals, (pid+i+1)%int(size))
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }

    // 如果其它proc也沒有可用元素,那么嘗試從vintim中獲取
    size = atomic.LoadUintptr(&p.victimSize)
    if uintptr(pid) >= size {
        return nil
    }
    locals = p.victim
    l := indexLocal(locals, pid)
    if x := l.private; x != nil { // 同樣的邏輯,先從vintim中的local private獲取
        l.private = nil
        return x
    }
    for i := 0; i < int(size); i++ { // 從vintim其它proc嘗試偷取
        l := indexLocal(locals, (pid+i)%int(size))
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }

    // 如果victim中都沒有,則把這個victim標(biāo)記為空,以后的查找可以快速跳過了
    atomic.StoreUintptr(&p.victimSize, 0)

    return nil
}
Put

這個方法用于將一個元素返還給 Pool,Pool 會把這個元素保存到池中,并且可以復(fù)用。但如果 Put 一個 nil 值,Pool 就會忽略這個值。

func (p *Pool) Put(x interface{}) {
    if x == nil { // nil值直接丟棄
        return
    }
    l, _ := p.pin()
    if l.private == nil { // 如果本地private沒有值,直接設(shè)置這個值即可
        l.private = x
        x = nil
    }
    if x != nil { // 否則加入到本地隊列中
        l.shared.pushHead(x)
    }
    runtime_procUnpin()
}

sync.Pool 最常用的一個場景:buffer 池

因?yàn)?byte slice 是經(jīng)常被創(chuàng)建銷毀的一類對象,使用 buffer 池可以緩存已經(jīng)創(chuàng)建的 byte slice。

var buffers = sync.Pool{
  New: func() interface{} { 
    return new(bytes.Buffer)
  },
}

func GetBuffer() *bytes.Buffer {
  return buffers.Get().(*bytes.Buffer)
}

func PutBuffer(buf *bytes.Buffer) {
  // 清空buf內(nèi)容
  buf.Reset()
  buffers.Put(buf)
}

sync.Pool 的坑

內(nèi)存泄漏

上面byte.Buffer會有內(nèi)存泄漏風(fēng)險

取出來的 bytes.Buffer 在使用的時候,我們可以往這個元素中增加大量的 byte 數(shù)據(jù),這會導(dǎo)致底層的 byte slice 的容量可能會變得很大。這個時候,即使 Reset 再放回到池子中,這些 byte slice 的容量不會改變,所占的空間依然很大。而且,因?yàn)?Pool 回收的機(jī)制,這些大的 Buffer 可能不被回收,而是會一直占用很大的空間,這屬于內(nèi)存泄漏的問題。

在使用 sync.Pool 回收 buffer 的時候,一定要檢查回收的對象的大小。如果 buffer 太大,就不要回收了,否則就太浪費(fèi)了。

內(nèi)存浪費(fèi)

除了內(nèi)存泄漏以外,還有一種浪費(fèi)的情況,就是池子中的 buffer 都比較大,但在實(shí)際使用的時候,很多時候只需要一個小的 buffer,這也是一種浪費(fèi)現(xiàn)象。

要做到物盡其用,盡可能不浪費(fèi)的話,我們可以將 buffer 池分成幾層。首先,小于 512 byte 的元素的 buffer 占一個池子;其次,小于 1K byte 大小的元素占一個池子;再次,小于 4K byte 大小的元素占一個池子。這樣分成幾個池子以后,就可以根據(jù)需要,到所需大小的池子中獲取 buffer 了。

總結(jié)

總結(jié)Pool 是一個通用的概念,也是解決對象重用和預(yù)先分配的一個常用的優(yōu)化手段。即使你自己沒在項目中直接使用過,但肯定在使用其它庫的時候,就享受到應(yīng)用 Pool 的好處了,比如數(shù)據(jù)庫的訪問、http API 的請求等等。

我們一般不會在程序一開始的時候就開始考慮優(yōu)化,而是等項目開發(fā)到一個階段,或者快結(jié)束的時候,才全面地考慮程序中的優(yōu)化點(diǎn),而 Pool 就是常用的一個優(yōu)化手段。如果你發(fā)現(xiàn)程序中有一種 GC 耗時特別高,有大量的相同類型的臨時對象,不斷地被創(chuàng)建銷毀,這時,你就可以考慮看看,是不是可以通過池化的手段重用這些對象。

另外,在分布式系統(tǒng)或者微服務(wù)框架中,可能會有大量的并發(fā) Client 請求,如果 Client 的耗時占比很大,你也可以考慮池化 Client,以便重用。

如果你發(fā)現(xiàn)系統(tǒng)中的 goroutine 數(shù)量非常多,程序的內(nèi)存資源占用比較大,而且整體系統(tǒng)的耗時和 GC 也比較高,我建議你看看,是否能夠通過 Worker Pool 解決大量 goroutine 的問題,從而降低這些指標(biāo)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容