1. 程式人生 > 其它 >Go sync.Pool 淺析

Go sync.Pool 淺析

sync.Pool 應該是 Go 裡面明星級別的資料結構,有很多優秀的文章都在介紹這個結構,本篇文章簡單剖析下 sync.Pool。不過說實話 sync.Pool 並不是我們日常開發中使用頻率很高的的併發原語。

儘管用的頻率很低,但是不可否認的是 sync.Pool 確實是 Go 的殺手鐗,合理使用 sync.Pool 會讓我們的程式效能飆升。本篇文章會從使用方式,原始碼剖析,運用場景等方面,讓你對 sync.Pool 有一個清晰的認知。

使用方式

sync.Pool 使用很簡單,但是想用對卻很麻煩,因為你有可能看到網上一堆錯誤的示例,各位同學在搜尋 sync.Pool 的使用例子時,要特別注意。

sync.Pool 是一個記憶體池。通常記憶體池是用來防止記憶體洩露的(例如C/C++)。sync.Pool 這個記憶體池卻不是幹這個的,帶 GC 功能的語言都存在垃圾回收 STW 問題,需要回收的記憶體塊越多,STW 持續時間就越長。如果能讓 new 出來的變數,一直不被回收,得到重複利用,是不是就減輕了 GC 的壓力。

正確的使用示例(下面的demo選自gin)

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c)
}

一定要注意的是:是先 Get 獲取記憶體空間,基於這個記憶體做相關的處理,然後再將這個記憶體還回(Put)到 sync.Pool。

Pool 結構

原始碼圖解

簡單點可以總結成下面的流程:

Sync.Pool 梳理

Pool 的內容會清理?清理會造成資料丟失嗎?

sync.Pool 會在每個 GC 週期內定期清理 sync.Pool 內的資料。定時清理資料並不會造成資料丟失。

要分幾個方面來說這個問題。

  1. 已經從 sync.Pool Get 的值,在 poolClean 時雖說將 pool.local 置成了nil,Get 到的值依然是有效的,是被 GC 標記為黑色的,不會被 GC回收,當 Put 後又重新加入到 sync.Pool 中
  2. 在第一個 GC 週期內 Put 到 sync.Pool 的數值,在第二個 GC 週期沒有被 Get 使用,就會被放在 local.victim 中。如果在 第三個 GC 週期仍然沒有被使用就會被 GC 回收。

runtime.GOMAXPROCS 與 pool 之間的關係?

s := p.localSize
l := p.local
if uintptr(pid) < s {
    return indexLocal(l, pid), pid
}

if p.local == nil {
    allPools = append(allPools, p)
}
// If GOMAXPROCS changes between GCs, we re-allocate the array and lose the old one.
size := runtime.GOMAXPROCS(0)
local := make([]poolLocal, size)
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
runtime_StoreReluintptr(&p.localSize, uintptr(size))     // store-release

runtime.GOMAXPROCS(0) 是獲取當前最大的 p 的數量。sync.Pool 的 poolLocal 數量受 p 的數量影響,會開闢 runtime.GOMAXPROCS(0) 個 poolLocal。某些場景下我們會使用 runtime.GOMAXPROCS(N) 來改變 p 的數量,會使 sync.Pool 的 pool.poolLocal 釋放重新開闢新的空間。

為什麼要開闢 runtime.GOMAXPROCS 個 local?

pool.local 是個 poolLocal 結構,這個結構體是 private + shared連結串列組成,在多 goroutine 的 Get/Put 下是有資料競爭的,如果只有一個 local 就需要加鎖來操作。每個 p 的 local 就能減少加鎖造成的資料競爭問題。

New() 的作用?假如沒有 New 會出現什麼情況?

從上面的 pool.Get 流程圖可以看出來,從 sync.Pool 獲取一個記憶體會嘗試從當前 private,shared,其他的 p 的 shared 獲取或者 victim 獲取,如果實在獲取不到時,才會呼叫 New 函式來獲取。也就是 New() 函式才是真正開闢記憶體空間的。New() 開闢出來的的記憶體空間使用完畢後,呼叫 pool.Put 函式放入到 sync.Pool 中被重複利用。

如果 New 函式沒有被初始化會怎樣呢?很明顯,sync.Pool 就廢掉了,因為沒有了初始化記憶體的地方了。

先 Put,再 Get 會出現什麼情況?

一定要注意,下面這個例子的用法是錯誤的

func main(){
    pool:= sync.Pool{
        New: func() interface{} {
            return item{}
        },
    }
    pool.Put(item{value:1})
    data := pool.Get()
    fmt.Println(data)
}

如果你直接跑這個例子,能得到你想像的結果,但是在某些情況下就不是這個結果了。

在 Pool.Get 註釋裡面有這麼一句話:“Callers should not assume any relation between values passed to Put and the values returned by Get.”,告訴我們不能把值 Pool.Put 到 sync.Pool 中,再使用 Pool.Get 取出來,因為 sync.Pool 不是 map 或者 slice,放入的值是有可能拿不到的,sync.Pool 的資料結構就不支援做這個事情。

前面說使用 sync.Pool 容易被錯誤示例誤導,就是上面這個寫法。為什麼 Put 的值 再 Get 會出現問題?

  • 情況1:sync.Pool 的 poolCleanup 函式在系統 GC 時會被呼叫,Put 到 sync.Pool 的值,由於有可能一直得不到利用,被在某個 GC 週期內就有可能被釋放掉了。
  • 情況2:不同的 goroutine 繫結的 p 有可能是不一樣的,當前 p 對應的 goroutine 放入到 sync.Pool 的值有可能被其他的 p 對應的 goroutine 取到,導致當前 goroutine 再也取不到這個值。
  • 情況3:使用 runtime.GOMAXPROCS(N) 來改變 p 的數量,會使 sync.Pool 的 pool.poolLocal 釋放重新開闢新的空間,導致 sync.Pool 被釋放掉。
  • 情況4:還有很多情況

只 Get 不 Put 會記憶體洩露嗎?

使用其他的池,如連線池,如果取連線使用後不放回連線池,就會出現連線池洩露,是不是 sync.Pool 也有這個問題呢?

通過上面的流程圖,可以看出來 Pool.Get 的時候會嘗試從當前 private,shared,其他的 p 的 shared 獲取或者 victim 獲取,如果實在獲取不到時,才會呼叫 New 函式來獲取,New 出來的內容本身還是受系統 GC 來控制的。所以如果我們提供的 New 實現不存在記憶體洩露的話,那麼 sync.Pool 是不會記憶體洩露的。當 New 出來的變數如果不再被使用,就會被系統 GC 給回收掉。

如果不 Put 回 sync.Pool,會造成 Get 的時候每次都呼叫的 New 來從堆疊申請空間,達不到減輕 GC 壓力。

使用場景

上面說到 sync.Pool 業務開發中不是一個常用結構,我們業務開發中沒必要假想某塊程式碼會有強烈的效能問題,一上來就用 sync.Pool 硬懟。 sync.Pool 主要是為了解決 Go GC 壓力過大問題的,所以一般情況下,當線上高併發業務出現 GC 問題需要被優化時,才需要用 sync.Pool 出場。

使用注意點

  1. sync.Pool 同樣不能被複制。
  2. 好的使用習慣,從 pool.Get 出來的值進行資料的清空(reset),防止垃圾資料汙染。

本文基於的 Go 原始碼版本:1.16.2

參考連結

  1. 深度解密 Go 語言之 sync.Poolhttps://www.cnblogs.com/qcrao-2018/p/12736031.html
  2. 請問sync.Pool有什麼缺點?https://mp.weixin.qq.com/s/2ZC1BWTylIZMmuQ3HwrnUg
  3. Go 1.13中 sync.Pool 是如何優化的?https://colobu.com/2019/10/08/how-is-sync-Pool-improved-in-Go-1-13/

sync.Pool 的剖析到這裡基本就寫完了,想跟我交流的可以在評論區留言。