最近碰到了在Go项目中垃圾回收的问题,很多对象大量的重复创建导致GC的压力很大,而使用sync.pool能减少重复的对象创建,降低GC的压力
sync.pool是什么呢
sync.pool是Go1.3发布的一个特性,它是一个临时对象存储池
为什么需要sync.pool呢
因为项目中频繁的创建对象和回收内存,造成了GC的压力;而sync.pool可以缓存对象暂时不用但是之后会用到的对象,并且不需要重新分配内存;这在很大程度上降低了GC的压力,并且提高了程序的性能
如何使用sync.Pool呢
首先,你需要为sync.Pool设置一个New函数,这个函数就是当你获取不到对象时,返回的默认值。接下来,你就可以通过Get和Put方法检索对象和临时存储对象了
注:你创建的这个pool在第一次使用过后就不能再被赋值了;还有就是Pool中的对象随时都会被移除,并且不会有通知机制。而如果你存储的是一个对象的引用,那么这个对象也会被回收
package main
import "sync"
type Person struct {
Name string
}
//Initialing pool
var personalPool = sync.Pool{
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
New: func() interface{} {
return &Person{}
},
}
func main() {
// Get hold of an instance
newPerson := personalPool.Get().(*Person)
// Defer release function
// After that the same instance is
// reusable by another routine
defer personalPool.Put(newPerson)
// using instance
newPerson.Name = "Jack"
}
Benchmark
package main
import (
"sync"
"testing"
)
type Person struct {
Age int
}
var (
personPool = sync.Pool{
New: func() interface{} {
return &Person{}
},
}
)
func BenchmarkWithoutPool(b *testing.B) {
var p *Person
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
p = new(Person)
p.Age = 23
}
}
}
func BenchmarkWithPool(b *testing.B) {
var p *Person
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
p = personPool.Get().(*Person)
p.Age = 23
personPool.Put(p)
}
}
}
测试结果:
cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
BenchmarkWithoutPool
BenchmarkWithoutPool 7933 146347 ns/op 80000 B/op 10000 allocs/op
cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
BenchmarkWithPool
BenchmarkWithPool 1864 595181 ns/op 0 B/op 0 allocs/op
Benchmark 参数说明
| 结果项 | 含义 |
|---|---|
| BenchmarkWithoutPool | BenchmarkWithoutPool 是测试的函数名 |
| 7933 | 表示一共执行了7933次,即b.N的值 |
| 146347 ns/op | 表示平均每次操作花费了146347纳秒 |
| 80000B/op | 表示每次操作申请了80000Byte的内存申请 |
| 10000 allocs/op | 表示每次操作申请了10000次内存 |
当然,这种重复的对象创建是比较合适使用sync.Pool对象的,但是对于一些简单的初始化动作,就不适合使用sync.Pool了,因为这也有一定的性能影响,所以需要选择合适的场景使用
sync.Pool的工作原理
sync.Pool有两个containers来存储对象,分别是:local pool和victim cache
根据sync.pool.go的源码中可以看出,这个package的init函数会注册一个PoolCleanUp函数,而这个函数就是通过GC触发的
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
当GC的触发的时候,在victim中的对象就会被收集回收,而在local pool中的对象会被移动victim cache当中;下面是poolCleanUp的代码:
func poolCleanup() {
// 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
}
oldPools, allPools = allPools, nil
}
新对象是放在local pool当中的,调用pool.Put也是将对象放在local pool当中的。
调用pool.Get时,会先从victim cache中获取,如果没有找到,则就会从local pool中获取,如果local pool中也没有,就会执行初始化时的New Functionle,否则就返回nil了

从Go1.12之后添加了mutex来保证线程安全;从Go1.13在sync.pool中引入了双向链表,移除了mutex,改善了共享操作
结论
总之,如果有一个对象需要频繁的创建,并且有很大的开销,那么你就可以使用sync.pool
参考:https://medium.com/swlh/go-the-idea-behind-sync-pool-32da5089df72