七分鐘讀懂 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

   local     unsafe.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,最後查找其他 Pshared,如果以上全部沒有可用元素,最後會調用 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 {
   // ... 省略了其他字段
   pool             sync.Pool
}

初始化 engine 時定義了 poolNew 函數:

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)
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章