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 內的資料。定時清理資料並不會造成資料丟失。
要分幾個方面來說這個問題。
- 已經從 sync.Pool Get 的值,在 poolClean 時雖說將 pool.local 置成了nil,Get 到的值依然是有效的,是被 GC 標記為黑色的,不會被 GC回收,當 Put 後又重新加入到 sync.Pool 中
- 在第一個 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 出場。
使用注意點
- sync.Pool 同樣不能被複制。
- 好的使用習慣,從 pool.Get 出來的值進行資料的清空(reset),防止垃圾資料汙染。
本文基於的 Go 原始碼版本:1.16.2
參考連結
- 深度解密 Go 語言之 sync.Poolhttps://www.cnblogs.com/qcrao-2018/p/12736031.html
- 請問sync.Pool有什麼缺點?https://mp.weixin.qq.com/s/2ZC1BWTylIZMmuQ3HwrnUg
- Go 1.13中 sync.Pool 是如何優化的?https://colobu.com/2019/10/08/how-is-sync-Pool-improved-in-Go-1-13/
sync.Pool 的剖析到這裡基本就寫完了,想跟我交流的可以在評論區留言。