7 minutes to read Go's temporary Object Pool pool and its application scenarios

  • 2020-06-23 00:37:51
  • OfStack

What is temporary Object pool pool?

sync.Pool gives a big comment to explain what pool is, so let's see what it says.

A temporary object pool is a collection of temporary objects that can be stored and fetched separately.

Objects in the pool are removed (released or retrieved for use) without any notice. If pool holds only 1 reference to an object, that object will most likely be reclaimed.

Pool is safe in multiple goroutine environments.

Pool is used to cache the currently unused memory that has been requested and may be used in the future to relieve GC pressure. It makes it easy and efficient to build the thread-safe free list (a data structure for dynamic memory requests). However, it is not suitable for free list for all scenarios.

Silently sharing a set of temporary elements between multiple independent threads running independently with 1 pool is a fair use scenario for pool. Pool provides a mechanism for sharing temporary elements between multiple independent client.

There is an example of using Pool in the fmt package, which maintains a dynamically sized output of buffer.

In addition, some short life cycle objects are not suitable for maintenance with pool, in which case it is not cost-effective to use pool. This is where they should use their own free list (probably referring to the go memory model for caching) < 32k small object free list) is more efficient.

Pool 1 cannot be copied once used.

Pool structure is defined as:


type Pool struct {
 noCopy noCopy

 local  unsafe.Pointer //  local P Cache pool pointer 
 localSize uintptr  //  local P Cache pool size 

 //  When there are no possible objects in the pool 
 //  Will be called  New  Functional construction 1 An object 
 New func() interface{}
}

There are two public methods defined in Pool, Put - adding elements to the pool; Get - Gets the element from the pool, calls New to generate the element if not, and returns nil if New is not set.

Get

Pool maintains one local pool for each P, and the local pool for P is divided into private and Shared pools private. Elements in the private pool can only be used locally by P. Elements in the Shared pool can be stolen by other P, so locking is not required when using the private pool private as compared to locking when using the Shared pool shared.

Get will first look for local private, then local shared, and finally shared for other P. If none of the above elements are available, it will finally call the New function to get the new element.


func (p *Pool) Get() interface{} {
 if race.Enabled {
  race.Disable()
 }
 //  Access to the local  P  the  poolLocal  object 
 l := p.pin() 
 
 //  First get  private  Objects in the pool (only 1 A) 
 x := l.private
 l.private = nil
 runtime_procUnpin()
 if x == nil {
  //  Look for local  shared  Pool, 
  //  local  shared  It could be something else  P  access 
  //  Need to lock 
  l.Lock()
  last := len(l.shared) - 1
  if last >= 0 {
   x = l.shared[last]
   l.shared = l.shared[:last]
  }
  l.Unlock()
  
  //  Look for other  P  the  shared  pool 
  if x == nil {
   x = p.getSlow()
  }
 }
 if race.Enabled {
  race.Enable()
  if x != nil {
   race.Acquire(poolRaceAddr(x))
  }
 }
 //  No available element found, call  New  generate 
 if x == nil && p.New != nil {
  x = p.New()
 }
 return x
}

getSlow to obtain available elements from shared pools in other P:


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))
  //  The corresponding  pool  Need to lock 
  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 prioritizes elements in the private pool; If private is not empty, it is placed in the shared pool. Interestingly, a quarter of the element may be thrown away before it is added to the pool.


func (p *Pool) Put(x interface{}) {
 if x == nil {
  return
 }
 if race.Enabled {
  if fastrand()%4 == 0 {
   //  Throw the elements away at random ...
   // 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 {
  //  Shared pool access, need to be locked 
  l.Lock()
  l.shared = append(l.shared, x)
  l.Unlock()
 }
 if race.Enabled {
  race.Enable()
 }
}

poolCleanup

poolCleanup is called when the world pauses and garbage collection is about to begin. Memory cannot be allocated within this function and no runtime functions can be called. The reason:
Prevent the entire Pool from being left in error

If an goroutine is accessing ES117en.shared when GC occurs, the entire Pool will be retained and the next time it executes, it will have double the memory


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{}
}

Case 1: Context pool in gin

In web application, the background creates a context Context for the current request when processing each request of the user, which is used to store the request information and corresponding information. Context has a long life cycle and user requests are in a concurrent environment, so the thread-safe Pool is ideal for maintaining temporary pools of Context objects.

Gin defines 1 pool in structure Engine:


type Engine struct {
 // ...  Other fields are omitted 
 pool    sync.Pool
}

The New function of pool was defined when initializing engine:


engine.pool.New = func() interface{} {
 return engine.allocateContext()
}

// allocateContext
func (engine *Engine) allocateContext() *Context {
 //  Construct a new context object 
 return &Context{engine: engine}
}

ServeHttp:


//  from  pool  , and convert to  *Context
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset() // reset

engine.handleHTTPRequest(c)

//  Throw back again  pool  In the 
engine.pool.Put(c)

Case 2: printer pool in fmt

printer also fits the long life cycle and is likely to be used in multiple goroutine, so pool is also suitable for maintenance.

printer with its temporary object pool


// pp  Used to maintain  printer  The state of the 
//  It does this by  sync.Pool  To reuse, to avoid asking for memory 
type pp struct {
 //...  Field omitted 
}

var ppFree = sync.Pool{
 New: func() interface{} { return new(pp) },
}

Capture and Release:


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)
}

conclusion


Related articles: