Go sync.Pool

330 阅读4分钟

简单介绍

通常在数据库连接、网络连接或其他具有持久性和资源消耗较高的资源下,我们会使用 Connection Pool(连接池) 降低建立和关闭连接的成本,并确保在需要时可以快速获取可用的连接。

同样在并发编程中,资源的分配和回收是一个很重要的问题,对于常用资源频繁的分配和回收,会造成大量性能的开销,sync.Pool 是 Go 语言标准库 sync 中提供的一个用于管理临时对象的 对象池,通过池化技术缓存和复用分配的临时对象,避免频繁地创建和销毁,减少程序GC的压力,以提升程序的性能。

由于 sync.Pool 是并发安全的,所以多个 goroutine 可以同时访问同一个 sync.Pool 对象,从而共享池中的对象,避免竞争条件

源码分析

基础结构

1696659882180.png

type Pool struct {
   noCopy noCopy    // 防拷贝标识

   local     unsafe.Pointer    // localPool,存储着各个 P 对应的本地对象池
   localSize uintptr           // 数组大小,等于 p 的个数

   victim     unsafe.Pointer  // 经过一轮 gc 之后,暂存上一轮的 localPool
   victimSize uintptr         // 数组大小,等于 p 的个数

   New func() any
}
type poolLocal struct {
   poolLocalInternal

   // Prevents false sharing on widespread platforms with
   // 128 mod (cache line size) = 0 .
   pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

// Local per-P Pool appendix.
type poolLocalInternal struct {
   private any           // P 的私有元素,操作时无需加锁
   shared  poolChain     // P 的共享元素链表
}

核心方法

pool.pin();将当前 goroutine 与 P 进行绑定,短暂处于不可抢占状态,以支持无锁化获取 private 中的元素

func (p *Pool) pin() (*poolLocal, int) {
   // 取出当前 P 的 index
   pid := runtime_procPin()

   s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
   l := p.local                              // load-consume
   if uintptr(pid) < s {
      return indexLocal(l, pid), pid
   }
   
   // 如果是首次调用 pin 方法,则会走进 pinSlow 方法
   return p.pinSlow()
}

pool.Get();从池中选择任意项目,将其从池中删除,然后将其返回给调用者

func (p *Pool) Get() any {
   if race.Enabled {
      race.Disable()
   }
   // 绑定
   l, pid := p.pin()  
   
   // 获取 private 的值,并将其置为 nil
   x := l.private     
   l.private = nil
   
   
   if x == nil {
      // 如果 private 中没有值,从 shared 的头部获取
      x, _ = l.shared.popHead()
      if x == nil {
         // 从其他 poolLocal 的 shared 里面偷取 --> 在 victim 中再执行一轮操作
         x = p.getSlow(pid)
      }
   }
   
   // Unpin()
   runtime_procUnpin()
   
   // 是否启用了数据竞争检测,默认为 false
   if race.Enabled {
      race.Enable()
      if x != nil {
         race.Acquire(poolRaceAddr(x))
      }
   }
   
   // 如果 Get 返回 nil,而 p.New 不是 nil,则 Get 返回调用 p.New()
   if x == nil && p.New != nil {
      x = p.New()
   }
   return x
}

pool.Put();将取出的对象放回池中

func (p *Pool) Put(x any) {
   if x == nil {
      return
   }
   if race.Enabled {
      if fastrandn(4) == 0 {
         // Randomly drop x on floor.
         return
      }
      race.ReleaseMerge(poolRaceAddr(x))
      race.Disable()
   }
   
   l, _ := p.pin()
   if l.private == nil {
      // 如果当前 poolLocal 的 private 是空的,直接放里面
      l.private = x
   } else {
      // 否则加到 shared 头部
      l.shared.pushHead(x)
   }
   
   runtime_procUnpin()
   
   if race.Enabled {
      race.Enable()
   }
}

垃圾回收

存入 Pool 的对象会不定期被 Go 运行时回收,最多两轮 GC,Pool 内的对象资源将会全部回收,因此即便大量存入元素,也不会发生内存泄露

但是需要持久化的东西最好还是不要放进去,比如数据库连接

func poolCleanup() {
   // 遍历 pools,回收 victim 中资源
   for _, p := range oldPools {
      p.victim = nil
      p.victimSize = 0
   }
   
   // 将 local 中资源转移到 victim 中
   for _, p := range allPools {
      p.victim = p.local
      p.victimSize = p.localSize
      p.local = nil
      p.localSize = 0
   }

   oldPools, allPools = allPools, nil
}

使用示例

下面是一个简单的 sync.Pool 使用示例;在创建 sync.Pool 时,需要传入一个 New() ,当 Get() 方法获取不到对象时,此时将会调用 New() 创建新的对象返回

type Worker struct {
   Name string
}

func main() {
   pool := sync.Pool{New: func() any {
      fmt.Printf("create a new worker----------> ")
      return &Worker{Name: "zhang3"}
   }}

   w1 := pool.Get().(*Worker)
   fmt.Println("1", w1)
   pool.Put(w1)

   w1 = pool.Get().(*Worker)
   fmt.Println("2", w1)

   w2 := pool.Get().(*Worker)
   fmt.Println("3", w2)

   pool.Put(w1)
   pool.Put(w2)
}


// create a new worker----------> 1 &{zhang3}
// 2 &{zhang3}                               
// create a new worker----------> 3 &{zhang3}

可以看到,第一次获取对象时,New() 被调用,创建了一个新的对象;然后,我们将对象归还到池中,第二次获取对象,这时应该从池中获取,而不是创建新的对象;第三次没有将之前的对象放回池中,所有又再次创建了新的对象。

工程实践

Context 对象常用来跟踪请求的上下文信息,频繁地创建和销毁可能会产生一定的性能开销。

在 web 框架 gin 中,定义了一个 gin.Context 来跟踪一个 http 请求,串联前后处理函数,并传递相关信息。gin.Context 作为一个固定的结构,并且每一次请求来了都需要创建,请求结束之后再进行销毁;那么一旦大量请求打入,可能会面临较大的 GC 压力,因此 gin 使用 sync.Pool 来复用 Context

func New() *Engine {
   debugPrintWARNINGNew()
   engine := &Engine{
   }
   engine.RouterGroup.engine = engine
   
   engine.pool.New = func() any {
      return engine.allocateContext(engine.maxParams)
   }
   
   return engine
}


func (engine *Engine) allocateContext(maxParams uint16) *Context {
   v := make(Params, 0, maxParams)
   skippedNodes := make([]skippedNode, 0, engine.maxSections)
   return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}