golang sync.Pool 的基本原理
sync.Pool 包寥寥不过300行代码,却可以为我们提供类似对象池的功能,减少了对象的重复创建于销毁,减少了性能损耗,增加内存复用,同时自带 mutex 锁可以保证 Put/Get 操作的并发安全特性,在一些对象需要重复创建销毁的场景中很是实用,今天来看看 sync.Pool 的基本原理。
1.源码解读
sync.Pool 就在标准库中,注释开篇就是(go 1.19):
// A Pool is a set of temporary objects that may be individually saved and // retrieved. // // Any item stored in the Pool may be removed automatically at any time without // notification. If the Pool holds the only reference when this happens, the // item might be deallocated. // // A Pool is safe for use by multiple goroutines simultaneously.
翻译过来就是:
1.对象池是提供一种存储、获取临时对象的集合 2.不要把对象池当做缓存来处理,因为任何存储的对象,在某个时刻都可能会被回收掉,而且没有任何通知的情况下 3.这个对象池的使用是并发安全的
继续往下看:
// Pool's purpose is to cache allocated but unused items for later reuse, // relieving pressure on the garbage collector. That is, it makes it easy to // build efficient, thread-safe free lists. However, it is not suitable for all // free lists. // // An appropriate use of a Pool is to manage a group of temporary items // silently shared among and potentially reused by concurrent independent // clients of a package. Pool provides a way to amortize allocation overhead // across many clients.
翻译过来:
1.对象池的目的是用来缓存那些已分配但是在后续的重复使用的对象,可以减轻GC压力 2.对象池一种合适的使用场景,是在多个客户端之间共享重用对象,得以分摊内存分配的开销
后面的注释也提到,其实在 fmt 包中也是有 Pool 使用的体现,内部维护了一个动态大小的临时输出 buffer 空间。
注意
需要注意的点:
1.短生命周期的对象不太适合放对象池中 2.对象池不要复制 3.一般的使用场景,先声明 New(),然后再去使用 Get()/Put() 方法,可以 put 任何对象,但在 get 对象的时候,需要用到断言,即 Get().(Type) 4.对象池中的对象数量不能做任何假设,什么时候新建什么时候被GC,这些都是不确定的,对象池主要是能尽可能复用对象,减少GC 5.Pool本身的数据结构是并发安全,但是 Pool.New() 方法需要使用者注意场景使用,如并发下,则加上并发安全策略,如原子操作或者加互斥锁等
开放的 API
sync.Pool 开放了以下的 api:
- New func() any,在创建 Pool 实例时需要填入对应的对象新建函数
- Get() any,从 Pool 获取相应对象,一般要通过断言获取到确定类型对象
- Put(any),用完对象后,直接放入对象池
下面主要看看 Put() 和 Get() 两个函数,也是我们用 Pool 用得最多的 API。
Put()
源码:
// Put adds x to the pool. func (p *Pool) Put(x any) { if x == nil { // 1.对象是空,直接返回 return } if race.Enabled { if fastrandn(4) == 0 { // Randomly drop x on floor. return } race.ReleaseMerge(poolRaceAddr(x)) race.Disable() } l, _ := p.pin() // 绑定 p,不可抢占(go v1.14后可抢占,防止 g 执行时间过长,长期占用 p),主要防止操作 put/get 时,对应到不同的 p,乱了顺序 if l.private == nil { // 2.如果对应 p 的 private 为空,直接放进 private 中,单纯的一个对象,类型也是 any l.private = x } else { l.shared.pushHead(x) // 3.private 已经有对象了,就放到 对应的 shared 共享队列(ring buffer)中,由于是绑定了 p 的,这里就直接操作对头插入,对应到 Get 中,就是 popHead } runtime_procUnpin() if race.Enabled { race.Enable() } }
所以我们看到 Put 的操作很简单,就是单纯地放回 Pool 中,涉及到不同的存放位置。基本上可以总结为下面的操作流程:
1、首先调用 p.pin() 抢占p,获取 localpool 2、优先放归还到 private 中 3、如果 private 有值,则放到 shared 中
Get()
源码:
// Get selects an arbitrary item from the Pool, removes it from the // Pool, and returns it to the caller. // Get may choose to ignore the pool and treat it as empty. // Callers should not assume any relation between values passed to Put and // the values returned by Get. // // If Get would otherwise return nil and p.New is non-nil, Get returns // the result of calling p.New. func (p *Pool) Get() any { if race.Enabled { race.Disable() } l, pid := p.pin() // 绑定 p,保持抢占,获取 localpool,通过 pid 进行后续操作 x := l.private // 1.优先从本地的private获取对象 l.private = nil // 置空 if x == nil { // 没有获取到,走其他方式获取 // Try to pop the head of the local shard. We prefer // the head over the tail for temporal locality of // reuse. x, _ = l.shared.popHead() // 2.从本地 p 的 shared 队列获取,因为是本地 p,所以可以直接 popHead if x == nil { // 在本地的 p 没有获取到,需要从其他的 p 窃取 x = p.getSlow(pid) // 3.从其他 p 上获取对象 } } runtime_procUnpin() if race.Enabled { race.Enable() if x != nil { race.Acquire(poolRaceAddr(x)) } } if x == nil && p.New != nil { // 上面的几条路径没有获取到,最后通过 New 重新创建对象 x = p.New() } return x }
注释见源码,我们可以看到,其实 Get() 方法也是有不同的优先级获取对象:
- 1.从本地 p 获取 private 对象
- 2.上面没有获取到,则从本地 p 的 shared 队列 队头获取对象
- 3.本地的 p 没有获取到,接下来从其他 p 获取,先从 victim 处获取,没有就走其他 p 的 shared队列的队尾 popTail 获取对象
- 4.上面的方式都没有获取到,最后通过传入的 New 兜底创建新对象
接下来继续追源码,一起来了解更多的细节。
pin() && pinSlow()
// pin pins the current goroutine to P, disables preemption and // returns poolLocal pool for the P and the P's id. // Caller must call runtime_procUnpin() when done with the pool. func (p *Pool) pin() (*poolLocal, int) { pid := runtime_procPin() // In pinSlow we store to local and then to localSize, here we load in opposite order. // Since we've disabled preemption, GC cannot happen in between. // Thus here we must observe local at least as large localSize. // We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness). s := runtime_LoadAcquintptr(&p.localSize) // load-acquire l := p.local // load-consume if uintptr(pid) < s { return indexLocal(l, pid), pid } return p.pinSlow() } func (p *Pool) pinSlow() (*poolLocal, int) { // Retry under the mutex. // Can not lock the mutex while pinned. runtime_procUnpin() allPoolsMu.Lock() defer allPoolsMu.Unlock() pid := runtime_procPin() // poolCleanup won't be called while we are pinned. s := p.localSize l := p.local if uintptr(pid) < s { return indexLocal(l, pid), pid } if p.local == nil { allPools = append(allPools, p) } // If GOMAXPROCS changes between GCs, we re-allocate the array and lose the old one. size := runtime.GOMAXPROCS(0) local := make([]poolLocal, size) atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release runtime_StoreReluintptr(&p.localSize, uintptr(size)) // store-release return &local[pid], pid }
这里涉及到的就是要保持抢占 p,在 pin() 中没有绑定到 p,就通过 pinSlow() 加 互斥锁 获取 p,其中也涉及一些 poolLocal 的新建。
Pool.getSlow(pid int) any
// 主要是从其他的 p 窃取对象 func (p *Pool) getSlow(pid int) any { // See the comment in pin regarding ordering of the loads. size := runtime_LoadAcquintptr(&p.localSize) // load-acquire locals := p.local // load-consume // Try to steal one element from other procs. for i := 0; i < int(size); i++ { // 遍历 p,看是否能从 其他 p 窃取到 对象,从 other p 的 tail 来 pop 对象 l := indexLocal(locals, (pid+i+1)%int(size)) if x, _ := l.shared.popTail(); x != nil { // 获取到对象就返回 return x } } // Try the victim cache. We do this after attempting to steal // from all primary caches because we want objects in the // victim cache to age out if at all possible. size = atomic.LoadUintptr(&p.victimSize) if uintptr(pid) >= size { return nil } locals = p.victim // 从存活一轮的 victim 中获取对象,只要获取到就返回对象 l := indexLocal(locals, pid) if x := l.private; x != nil { // 先看 private,没有再从 shared队列获取 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 } } // Mark the victim cache as empty for future gets don't bother // with it. atomic.StoreUintptr(&p.victimSize, 0) // 走到这里说明没有获取到对象,victimSize 置 0 return nil }
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 optionally specifies a function to generate // a value when Get would otherwise return nil. // It may not be changed concurrently with calls to Get. New func() any }
源码解读:
- noCopy,表示对象不可复制
- local,[P]poolLocal,固定数量的 p 的对象储存,每个元素即 private && shared 队列
- localSize,p 的数量
- victim,一轮 GC 后,local -> victim,实际就是上轮的 local 转为 victim ,local = nil,缓存的临时对象多存活一轮
- victimSize,同 localSize
- New,即定义创建新对象的方法
需要注意的是, Pool 本身是并发安全的,通过大量的原子操作及本地 p 的无锁操作实现无锁,但是 New 可能不是并发安全的,需要使用者自己定义。
相关结构体
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 } // Local per-P Pool appendix. // 每个 p 上独有的结构,包含一个任意类型的 private 和 一个 环形队列的 poolChain type poolLocalInternal struct { private any // Can be used only by the respective P. // p自己可操作的 private shared poolChain // Local P can pushHead/popHead; any P can popTail. // 绑定 p 的操作限于队头的pop/push,其他 p 则从队尾操作,popTail }
poolCleanup()
// 此函数或在 GC 前注册到 gc 处理中,由于 gc 也是由 runtime 控制的,所以是无法预计对象何时被回收 func poolCleanup() { // This function is called with the world stopped, at the beginning of a garbage collection. // It must not allocate and probably should not call any runtime functions. // Because the world is stopped, no pool user can be in a // pinned section (in effect, this has all Ps pinned). // Drop victim caches from all pools. // 回收 victim 对象,去除引用 for _, p := range oldPools { p.victim = nil p.victimSize = 0 } // Move primary cache to victim cache. for _, p := range allPools { // 注意此处的处理,local -> victim,相当于 local 的对象来到 victim 后,最多多活一轮 GC p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } // The pools with non-empty primary caches now have non-empty // victim caches and no pools have primary caches. oldPools, allPools = allPools, nil // 处理后,原来的 pools 就变成了 oldPools } // 初始化时就注册进运行时的处理函数 func init() { runtime_registerPoolCleanup(poolCleanup) }
上面是个人的一些解读,如想了解更详细,请前往下方的参考文档。
2.更多
下面说说 sync.Pool 的一些注意事项。
1.为什么用 Pool,而不是在运行的时候直接实例化对象呢?
原因:Go 的内存释放是由 runtime 来自动处理的,有 GC 过程。
创建 Pool 实例的时候,只要求填充了 New 函数,而根本没有声明或者限制这个 Pool 的大小。所以,记住一点,程序员作为使用方不能对 Pool 里面的元素个数做假定。如果速度足够快,可能里面可以只有一个元素就可以服务 若干个 个并发的 Goroutine 。
2.sync.Pool 是并发安全的吗?
本质原因:Pool.New 函数可能会被并发调用。
因为 sync.Pool 只是本身的 Pool 数据结构是并发安全的,并不是说 Pool.New 函数一定是线程安全的。Pool.New 函数可能会被并发调用 ,如果 New 函数里面的实现是非并发安全的,那就会有问题。
3.为什么 sync.Pool 不适合用于像 socket 长连接或数据库连接池?
因为,我们不能对 sync.Pool 中保存的元素做任何假设,以下事情是都可以发生的:
Pool 池里的元素随时可能释放掉,释放策略完全由 runtime 内部管理;
Get 获取到的元素对象可能是刚创建的,也可能是之前创建好 cache 住的。使用者无法区分;
Pool 池里面的元素个数你无法知道;
所以,只有的你的场景满足以上的假定,才能正确的使用 Pool 。sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担。划重点:临时对象。所以说,像 socket 这种带状态的,长期有效的资源是不适合 Pool 的。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10亿数据,如何做迁移?
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 易语言 —— 开山篇
· Trae初体验