sync.pool使用介绍

背景

正常情况下,一个函数里创建一个对象时会分配新的内存空间,假如这个函数被频繁调用,那么就会频繁地创建新对象,分配新的内存空间。如果这个变量发生逃逸,那就会对GC产生比较大的负担,因此我们自然而然地就会思考:对象能不能复用?即创建对象并使用完后再次被使用,同时也考考虑到多个协程同时复用一个对象的场景。

sync.pool就是为了解决上述问题。

示例

package main

import (
	"runtime"
	"sync"
	"time"
)

func main() {
	runtime.GOMAXPROCS(2)
	var pool = sync.Pool{
		New: func() interface{} {
			return 0
		},
	}
	go func() {
		pool.Put(1) // 放到私有对象里
		// pool.Put(2) // 放到共享池里
		// pool.Put(3) // 追加到共享池里
	}()
	time.Sleep(1 * time.Second)
	go func() {
		// 如果本地私有对象和共享池里都没有,从其他协程对应P的共享池中获取
		println(pool.Get().(int))
	}()
	time.Sleep(1 * time.Second)
}

这里起了两个协程,协程1放入了一个对象到私有对象里,协程2有一半的几率获取到0, 一半的几率获取到1。

接下来介绍一下sync.pool的底层原理:

结构体

Pool

type Pool struct {
	noCopy noCopy

	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array

	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array
	New func() any
}

noCopy:和sync.WaitGroup中的结构体一样,也是禁止拷贝;

local:是个数组,长度为 P 的个数。其元素类型是poolLocal ,可以近似地看做[P]poolLocal

localSize:P的个数;

victim:一次GC后并不会把对象清除,而是会把local里的数据拷贝到这里, 这样做是为了防止 GC 之后 sync.Pool 被突然清空,对程序性能造成影响。如果在其他的P中取不到数据,最后也可以从 victim 中取,这样程序性能会更加平滑;

victimSize:和victim一起使用;

New:一个保底的方法,在获取对象是,依次从本地P的私有对象->本地P的共享池->其他P的共享池->victim里获取,如果都没有取到(比如多个协程同时取),则调用该方法创建一个。

poolLocal

type poolLocal struct {
   poolLocalInternal

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

每个P维护一个poolLocal

poolLocalInternal

type poolLocalInternal struct {
   private any       // Can be used only by the respective P.
   shared  poolChain // Local P can pushHead/popHead; any P can popTail.
}

private:私有对象;

shared:共享池,是一个双向链表。

poolChain

type poolChain struct {
   head *poolChainElt
   tail *poolChainElt
}

poolChainElt

type poolChainElt struct {
   poolDequeue
   next, prev *poolChainElt
}

最终的结构图如下:

介绍完结构体后再看看它实现的几个方法:

Put()

func (p *Pool) Put(x any) {
   if x == nil {
      return
   }
   // ...
   l, _ := p.pin()
   if l.private == nil { // 如果私有值没有,则设置私有值
      l.private = x
   } else {
      l.shared.pushHead(x) // 否则追加到该协程对应的共享池里
   }
   runtime_procUnpin()
   if race.Enabled {
      race.Enable()
   }
}
  1. 将当前goroutine钉在当前的P上(禁止抢占)并返回对应的poolLocal
  2. 如果自己的local里没有设置私有对象,则设置;
  3. 否则追加到local的共享池的头部;
  4. 解除禁止抢占。

再看看pin()方法做了什么:

pin()

func (p *Pool) pin() (*poolLocal, int) {
	// 将当前goroutine订到当前P上,并返回当前P的序号
	pid := runtime_procPin()
	// 获取当前的localSize
	s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
	l := p.local                              // load-consume
	if uintptr(pid) < s {
		// 如果pid大于等于localSize,则通过pid找到对应索引位置的poolLocal
		return indexLocal(l, pid), pid
	}
	// 如果pid大于等于localSize,则需要创建新的poolLocal,来保证每个P都有一个poolLocal
	return p.pinSlow()
}
  1. 先将当前goroutine订到当前P上,并返回当前P的序号;
  2. 获取当前的localSize,第一次put的时候localSize=0,则需要创建新的poolLocal,来保证每个P都有一个poolLocal,同时修改localSize
  3. 如果pid的索引小于localSize,则直接返回local对应的索引位置的poolLocal.

Get()

func (p *Pool) Get() any {
   if race.Enabled {
      race.Disable()
   }
   l, pid := p.pin()
   x := l.private  // 先从本地取
   l.private = nil // 取完后删除

   if x == nil {
      x, _ = l.shared.popHead() // 如果本地没有,则从共享池里取
      if x == nil {
         x = p.getSlow(pid) // 如果共享池里没有则从其他协程偷取
      }
   }
   runtime_procUnpin()
   // ...
   if x == nil && p.New != nil { // 如果都没取到,则调用New()方法生成一个
      x = p.New()
   }
   return x
}
  1. 将当前goroutine钉在当前的P上(禁止抢占)并返回对应的poolLocal
  2. 先获取当前P中的私有对象,如果有则返回;
  3. 如果获取不到则从当前P的共享池中的尾部获取;
  4. 如果还获取不到,则从其他P的共享池中获取;
  5. 如果还获取不到,则从victim获取:
  6. 解除禁止抢占;
  7. 此时如果还没有获取到,则调用New方法创建新的对象并返回。

下面看看getSlow()是怎么从其他P获取对象的:

getSlow()

// 从其它P的共享池里获取对象
func (p *Pool) getSlow(pid int) any {
   size := runtime_LoadAcquintptr(&p.localSize) // load-acquire
   locals := p.local                            // load-consume
   // 遍历locals列表,从其他local的共享池的尾部获取对象
   for i := 0; i < int(size); i++ {
      l := indexLocal(locals, (pid+i+1)%int(size))
      if x, _ := l.shared.popTail(); x != nil {
         return x
      }
   }
   size = atomic.LoadUintptr(&p.victimSize)
   if uintptr(pid) >= size {
      return nil
   }
   // 如果从locals取不到则从victim取
   locals = p.victim
   l := indexLocal(locals, pid)
   // 先从自己的私有对象里取
   if x := l.private; x != nil {
      l.private = nil
      return x
   }
   // 再从其他的共享池的尾部取
   for i := 0; i < int(size); i++ {
      l := indexLocal(locals, (pid+i)%int(size))
      if x, _ := l.shared.popTail(); x != nil {
         return x
      }
   }
   // 取不到则将victimSize置位0,下次就不会再从victim中取了
   atomic.StoreUintptr(&p.victimSize, 0)
   return nil
}
  1. 将locals置为p.local;
  2. 遍历locals中所有的poolLocal,如果是跟自己绑定的P的poolLocal,则获它的取私有对象;
  3. 如果获取不到,则从共享池的尾部获取;
  4. 如果还是获取不到,则将locals置为p.victim,并重复步骤2、3
  5. 如果还是取不到,则将victimSize置为0,下次就不会再从victim中取了(下次GC之前)。

总结

总结来说,sync.Pool 利用以下手段将程序性能做到了极致:

  1. 利用 GMP 的特性,为每个 P 创建了一个本地对象池 poolLocal,尽量减少并发冲突;
  2. 每个 poolLocal 都有一个 private 对象,优先存取 private 对象,可以避免进入复杂逻辑;
  3. 在 Get 和 Put 期间,利用 pin 锁定当前 P,防止 goroutine 被抢占,造成程序混乱;
  4. 在获取对象期间,利用对象窃取的机制,从其他 P 的本地对象池以及 victim 中获取对象;
  5. 充分利用 CPU Cache 特性,提升程序性能。
posted @   独揽风月  阅读(1263)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
点击右上角即可分享
微信分享提示