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 的。