七分钟读懂 Go 的临时对象池pool及其应用场景

栏目: Go · 发布时间: 7年前

内容简介:临时对象池是一些可以分别存储和取出的临时对象。池中的对象会在没有任何通知的情况下被移出(释放或者重新取出使用)。如果

临时对象池 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 ,最后查找其他 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 {
   // ... 省略了其他字段
   pool             sync.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)
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

Pro CSS Techniques

Pro CSS Techniques

Jeff Croft、Ian Lloyd、Dan Rubin / Apress / 2009-5-4 / GBP 31.49

Web Standards Creativity: Innovations in Web Design with CSS, DOM Scripting, and XHTML一起来看看 《Pro CSS Techniques》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具