Go sync-Pool 淺析

hi, 大家好,我是 haohongfan。

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.Get

Pool.Put

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

Pool.Get 流程

Pool.Put 流程

Pool GC 流程

Sync.Pool 梳理

Pool 的內容會清理?清理會造成數據丟失嗎?

Go 會在每個 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 會出現問題?

只 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.Pool https://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/

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