介绍
sync.Pool
是 sync 包提供的一个数据类型,也称为临时对象池,它的值是用来存储一组可以独立访问的临时对象,它通过池化减少申请新对象,提升程序的性能。sync.Pool
类型是 struct 类型,它的值在被首次使用之后,就不可以再被复制了。因为 sync.Pool
中存储的所有对象都可以随时自动删除,所以使用 sync.Pool
类型的值必须满足两个条件,一是该值存在与否,都不会影响程序的功能,二是该值之间可以互相替代。sync.Pool
是 goroutine 并发安全的,可以安全地同时被多个 goroutine 使用;sync.Pool
的目的是缓存已分配但未使用的对象以供以后重用,从而减轻了垃圾收集器的性能影响,因为 Go 的自动垃圾回收机制,会有一个 STW 的时间消耗,并且大量在堆上创建对象,也会增加垃圾回收标记的时间。
sync.Pool
的适当用法是管理一组临时对象,这些临时对象在程序包的并发独立客户端之间静默共享并有可能被重用。sync.Pool
提供了一种分摊许多客户端上的分配开销的方法。
但是,作为短期(short-lived)对象的一部分维护的空闲列表不适用于 sync.Pool
,因为在这种情况下,开销无法很好地分摊。
Golang 语言中的标准库 fmt 包使用了 sync.Pool
,它会使用一个动态大小的 buffer 池做输出缓存,当大量的 goroutine 并发输出的时候,就会创建比较多的 buffer,并且在不需要的时候回收掉。
02
使用方式
sync.Poll
类型包含两个方法:
- func (p *Pool) Put(x interface{})
- func (p *Pool) Get() interface{}
Put()
用于向临时对象池中存放对象,它接收一个 interface{}
空接口类型的参数;Get()
用于从临时对象池中获取对象,它返回一个 interface{}
空接口类型的返回值。
Get()
从临时对象池中选择一个任意对象,将其从临时对象池中删除,然后将其返回给调用方。 Get()
可以选择忽略临时对象池并将其视为空。调用者不应假定传递给 Put()
的值和 Get()
返回的值之间有任何关系。
如果 Get()
返回 nil,而 p.New
不为 nil,则 Get()
返回调用 p.New
的结果。 sync.Pool
类型的 New
字段,字段类型是函数类型 func() interface{}
,代表创建临时对象的函数,该函数的结果值并不会存入到临时对象池中,而是直接返回给 Get()
方法的调用方。
需要注意的是,sync.Pool
类型的 New
字段的值也需要我们初始化对象时给定,否则,在调用 Get()
方法时,有可能会得到 nil。
我们已经介绍了临时对象什么时候会被创建,现在我们介绍临时对象什么时候会被销毁。我们已经知道 sync.Pool
使用之前需要先初始化,其实在初始化时,还会向 Golang 运行时中注册一个清理函数,用于清理临时对象池中的所有已创建的值,golang 运行时每次在执行垃圾回收之前,先执行该清理函数。
示例代码:
func main () { pool := &sync.Pool{ New: func() interface{} { fmt.Println("New 一个新对象") return 0 }, } // 取,临时对象池中没有数据,会调用 New,New 创建一个新对象直接返回,不会存储在临时对象池中 val := pool.Get().(int) fmt.Println(val) // 存 pool.Put(10) // 手动调用 GC(),用于验证 GC 之后,临时对象池中的对象会被清空。 runtime.GC() // 取 val2 := pool.Get().(int) fmt.Println(val2) }
03
实现原理
在 Go1.13 之前,临时对象池的数据结构中有一个本地池列表,在每个本地池中包含三个字段,分别是存储私有临时对象的字段 private、共享临时对象列表的字段 shared 和 sync.Mutex 类型的嵌入字段。
锁竞争会降低程序的并发性能,想要优化程序的并发性能,就是减少或避免锁的使用。在 Go1.13 中,sync.Pool
做了优化,就是避免使用锁,将加锁的队列改成了无锁的队列,并给即将被移除的元素多一次“复活”的机会。
当前 sync.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() interface{} }
其中 local 和 victim 主要用于存储空闲元素,每次 GC 时,Pool 会先把 victim 字段的数据移除,然后把 local 字段的数据给 victim,这样 local 等于被清空了,而 local 的数据在 victim 中就有机会再次被 Get()
取走,如果没有 Get()
取走数据,victim 的数据就会被 GC 掉。
阅读下面这段代码,它是 GC 时 sync.Pool
的处理逻辑。
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. for _, p := range oldPools { p.victim = nil p.victimSize = 0 } // Move primary cache to victim cache. for _, p := range allPools { 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 }
本地池列表中本地池的数量和 golang 调度器中 processor 的数量相等,也就是说每个本地池对应一个 P,我们在介绍 GMP 的文章中讲过,一个 goroutine 想要运行,必须先和某个 P 关联。所以临时对象池的 Put()
和 Get()
方法被调用时,会去操作哪个本地池,就取决于调用代码运行的 goroutine 对应的 P,这就是为什么每个本地池对应一个 P。
// Local per-P Pool appendix. type poolLocalInternal struct { private interface{} // Can be used only by the respective P. shared poolChain // Local P can pushHead/popHead; any P can popTail. } type poolLocal struct { poolLocalInternal // Prevents false sharing on widespread platforms with // 128 mod (cache line size) = 0 . pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte }
阅读上面这段代码,poolLocalInternal 结构体中包含两个字段 private 和 shared,private 代表一个缓存元素,只能被当前 P 的 goroutine 存取,因为一个 P 同时只能执行一个 goroutine,所以不会有并发问题。shared 可以被任意 P 访问,但是只能本地 P 可以 pushHead/popHead
,其他 P 只能 popTail
,它是使用一个无锁队列实现的。
存取数据:
Put()
方法会优先将新创建的临时对象存储在本地的 private 字段,如果 private 字段已经存储了某个值,它才会去访问 shared 字段,把新的临时对象追加到共享临时对象列表的末尾。
Get()
方法会优先访问 private 字段获取数据,因为无锁,获取元素的速度快,如果 private 字段为空时,就会尝试访问 local 的 shared 字段,如果 local 的 shared 字段也是空的,它会调用 getSlow()
方法,遍历每一个 local 的 shared 字段,只要发现某个 local 的 shared 字段有值,就会获取该 shared 共享临时对象列表的最后一个值并返回。如果遍历所有 local 都没有找到值,就会尝试访问 victim,先从 victim 的 private 字段中查找,如果没有找到,再从 victim 的 shared 字段查找,最后,如果都没有获取到,就会调用初始化时的 New 字段给定的创建临时对象的函数创建一个新对象并返回,如果 New 字段的值为 nil,Get()
方法就直接返回 nil。
getSlow()
方法的处理逻辑:
func (p *Pool) getSlow(pid int) interface{} { // 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++ { 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 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 } } // Mark the victim cache as empty for future gets don't bother // with it. atomic.StoreUintptr(&p.victimSize, 0) return nil }
04
总结
本文我们主要介绍了 sync.Pool
数据类型,包括它的使用方式和实现原理,它的优势就是可以复用对象,降低对象的新建和 GC 的开销。我们需要再次强调的是,sync.Pool
的生命周期受 GC 的影响,不适合用来做需要自己管理生命周期的池化,比如连接池。
推荐阅读:
参考资料: