七分鐘讀懂 Go 的臨時物件池pool及其應用場景
臨時物件池 pool 是啥?
sync.Pool
給了一大段註釋來說明pool
是啥,我們看看這段都說了些什麼。
臨時物件池是一些可以分別儲存和取出的臨時物件。
池中的物件會在沒有任何通知的情況下被移出(釋放或者重新取出使用)。如果pool
中持有某個物件的唯一引用,則該物件很可能會被回收。
Pool
在多goroutine
使用環境中是安全的。
Pool
是用來快取已經申請了的 目前未使用的 接下來可能會使用的 記憶體,以此緩解GC
壓力。使用它可以方便高效的構建執行緒安全的free list
(一種用於動態記憶體申請的資料結構)。然而,它並不適合所有場景的free list
。
在同一package
中獨立執行的多個獨立執行緒之間靜默共享一組臨時元素才是pool
的合理使用場景。Pool
提供在多個獨立client
之間共享臨時元素的機制。
在fmt
包中有一個使用Pool
的例子,它維護了一個動態大小的輸出buffer
。
另外,一些短生命週期的物件不適合使用pool
來維護,這種情況下使用pool
不划算。這是應該使用它們自己的free list
(這裡可能指的是 go 記憶體模型中用於快取 <32k小物件的 free list) 更高效。
Pool
一旦使用,不能被複制。
Pool
結構體的定義為:
type Pool struct { noCopy noCopy localunsafe.Pointer // 本地P快取池指標 localSize uintptr// 本地P快取池大小 // 當池中沒有可能物件時 // 會呼叫 New 函式構造構造一個物件 New func() interface{} }
Pool
中有兩個定義的公共方法,分別是Put
- 向池中新增元素;Get
- 從池中獲取元素,如果沒有,則呼叫New
生成元素,如果New
未設定,則返回nil
。
Get
Pool
會為每個P
維護一個本地池,P
的本地池分為 私有池private
和共享池shared
。私有池中的元素只能本地P
使用,共享池中的元素可能會被其他P
偷走,所以使用私有池private
時不用加鎖,而使用共享池shared
時需加鎖。
Get
會優先查詢本地private
,再查詢本地shared
,最後查詢其他P
的shared
,如果以上全部沒有可用元素,最後會呼叫New
函式獲取新元素。
func (p *Pool) Get() interface{} { if race.Enabled { race.Disable() } // 獲取本地 P 的 poolLocal 物件 l := p.pin() // 先獲取 private 池中的物件(只有一個) x := l.private l.private = nil runtime_procUnpin() if x == nil { // 查詢本地 shared 池, // 本地 shared 可能會被其他 P 訪問 // 需要加鎖 l.Lock() last := len(l.shared) - 1 if last >= 0 { x = l.shared[last] l.shared = l.shared[:last] } l.Unlock() // 查詢其他 P 的 shared 池 if x == nil { x = p.getSlow() } } if race.Enabled { race.Enable() if x != nil { race.Acquire(poolRaceAddr(x)) } } // 未找到可用元素,呼叫 New 生成 if x == nil && p.New != nil { x = p.New() } return x }
getSlow
,從其他P
中的shared
池中獲取可用元素:
func (p *Pool) getSlow() (x interface{}) { // See the comment in pin regarding ordering of the loads. size := atomic.LoadUintptr(&p.localSize) // load-acquire local := p.local// load-consume // Try to steal one element from other procs. pid := runtime_procPin() runtime_procUnpin() for i := 0; i < int(size); i++ { l := indexLocal(local, (pid+i+1)%int(size)) // 對應 pool 需加鎖 l.Lock() last := len(l.shared) - 1 if last >= 0 { x = l.shared[last] l.shared = l.shared[:last] l.Unlock() break } l.Unlock() } return x }
Put
Put
優先把元素放在private
池中;如果private
不為空,則放在shared
池中。有趣的是,在入池之前,該元素有 1/4 可能被丟掉。
func (p *Pool) Put(x interface{}) { if x == nil { return } if race.Enabled { if fastrand()%4 == 0 { // 隨機把元素扔掉... // Randomly drop x on floor. return } race.ReleaseMerge(poolRaceAddr(x)) race.Disable() } l := p.pin() if l.private == nil { l.private = x x = nil } runtime_procUnpin() if x != nil { // 共享池訪問,需要加鎖 l.Lock() l.shared = append(l.shared, x) l.Unlock() } if race.Enabled { race.Enable() } }
poolCleanup
當世界暫停,垃圾回收將要開始時,poolCleanup
會被呼叫。該函式內不能分配記憶體且不能呼叫任何執行時函式。原因:
Pool
如果GC
發生時,某個goroutine
正在訪問l.shared
,整個Pool
將會保留,下次執行時將會有雙倍記憶體
func poolCleanup() { for i, p := range allPools { allPools[i] = nil for i := 0; i < int(p.localSize); i++ { l := indexLocal(p.local, i) l.private = nil for j := range l.shared { l.shared[j] = nil } l.shared = nil } p.local = nil p.localSize = 0 } allPools = []*Pool{} }
案例1:gin 中的 Context pool
在web
應用中,後臺在處理使用者的每條請求時都會為當前請求建立一個上下文環境Context
,用於儲存請求資訊及相應資訊等。Context
滿足長生命週期的特點,且使用者請求也是屬於併發環境,所以對於執行緒安全的Pool
非常適合用來維護Context
的臨時物件池。
Gin
在結構體Engine
中定義了一個pool
:
type Engine struct { // ... 省略了其他欄位 poolsync.Pool }
初始化engine
時定義了pool
的New
函式:
engine.pool.New = func() interface{} { return engine.allocateContext() } // allocateContext func (engine *Engine) allocateContext() *Context { // 構造新的上下文物件 return &Context{engine: engine} }
ServeHttp:
// 從 pool 中獲取,並轉化為 *Context c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset()// reset engine.handleHTTPRequest(c) // 再扔回 pool 中 engine.pool.Put(c)
案例2:fmt 中的 printer pool
printer
也符合長生命週期的特點,同時也會可能會在多goroutine
中使用,所以也適合使用pool
來維護。
printer
與 它的臨時物件池
// pp 用來維護 printer 的狀態 // 它通過 sync.Pool 來重用,避免申請記憶體 type pp struct { //... 欄位已省略 } var ppFree = sync.Pool{ New: func() interface{} { return new(pp) }, }
獲取與釋放:
func newPrinter() *pp { p := ppFree.Get().(*pp) p.panicking = false p.erroring = false p.fmt.init(&p.buf) return p } func (p *pp) free() { p.buf = p.buf[:0] p.arg = nil p.value = reflect.Value{} ppFree.Put(p) }