1. 简介
作用:频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺,而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。
为了使得在多个goroutine中高效的使用goroutine,sync.Pool为每个P(对应CPU)都分配一个本地池,
当执行Get或者Put操作的时候,会先将goroutine和某个P的子池关联,再对该子池进行操作。
每个P的子池分为私有对象和共享列表对象,私有对象只能被特定的P访问,共享列表对象可以被任何P访问。
因为同一时刻一个P只能执行一个goroutine,所以无需加锁,但是对共享列表对象进行操作时,
因为可能有多个goroutine同时操作,所以需要加锁。
Put方法:
如果放入的值为空,直接return.
检查当前goroutine的是否设置对象池私有值,如果没有则将x赋值给其私有成员,并将x设置为nil。
如果当前goroutine私有值已经被设置,那么将该值追加到共享列表。
Get方法:
尝试从本地P对应的那个本地池中获取一个对象值, 并从本地池冲删除该值。
如果获取失败,那么从共享池中获取, 并从共享队列中删除该值。
如果获取失败,那么从其他P的共享池中偷一个过来,并删除共享池中的该值(p.getSlow())。
如果仍然失败,那么直接通过New()分配一个返回值,注意这个分配的值不会被放入池中。
New()返回用户注册的New函数的值,如果用户未注册New,那么返回nil。
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
可以看到在init的时候会向GC注册一个PoolCleanup函数,他会清除掉sync.Pool中的所有的缓存的对象,
这个注册函数会在每次GC的时候运行,所以sync.Pool中的值只在两次GC中间的时段有效。
通过以上的解读,我们可以看到,Get方法并不会对获取到的对象值做任何的保证,
因为放入本地池中的值有可能会在任何时候被删除,但是不通知调用者。放入共享池中的值有可能被其他的goroutine偷走。
所以对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,
因为存入对象池重的值有可能会在垃圾回收时被删除掉,这违反了数据库连接池建立的初衷。
根据上面的说法,Golang的对象池严格意义上来说是一个临时的对象池,适用于储存一些会在goroutine间分享的临时对象。主要作用是减少GC,提高性能。
其实sync.pool有点像python线程间共享变量的实现,不同线程间共享变量来实现对象的复用。
2. 使用场景
举个简单的例子:
type Student struct {
Name string
Age int32
Remark [1024]byte
}
var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25})
func unmarsh() {
stu := &Student{}
json.Unmarshal(buf, stu)
}
json 的反序列化在文本解析和网络通信过程中非常常见,当程序并发度非常高的情况下,短时间内需要创建大量的临时对象。而这些对象是都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。
3. 如何使用
sync.Pool 的使用方式非常简单:
3.1 声明对象池
只需要实现 New 函数即可。对象池中没有对象时,将会调用 New 函数创建。
var studentPool = sync.Pool{
New: func() interface{} {
return new(Student)
},
}
3.2 Get & Put
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
- Get()用于从对象池中获取对象,因为返回值是interface{}`,因此需要类型转换。
- Put()` 则是在对象使用完毕后,返回对象池。
4. 性能测试
4.1 struct 反序列化
func BenchmarkUnmarshal(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := &Student{}
json.Unmarshal(buf, stu)
}
}
func BenchmarkUnmarshalWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
}
}
var stu = new(Student)
func BenchmarkUnmarshalWithGlobal(b *testing.B) {
for n := 0; n < b.N; n++ {
json.Unmarshal(buf, stu)
}
}
测试结果如下:
$ go test -bench="^BenchmarkUnmar" -benchmem
goos: darwin
goarch: amd64
pkg: example/hpg-sync-pool
BenchmarkUnmarshal-8 9277 131042 ns/op 1384 B/op 7 allocs/op
BenchmarkUnmarshalWithPool-8 9441 130150 ns/op 232 B/op 6 allocs/op
BenchmarkUnmarshalWithGlobal-8 9860 136932 ns/op 232 B/op 6 allocs/op
PASS
ok example/hpg-sync-pool 2.334s
在这个例子中,因为 Student 结构体内存占用较小,内存分配几乎不耗时间。而标准库 json 反序列化时利用了反射,效率是比较低的,占据了大部分时间,因此两种方式最终的执行时间几乎没什么变化。但是内存占用差了一个数量级,第一种没有使用池,每执行一次用例都比后两者多一次内存分配,而第二个使用了 sync.Pool 后,内存占用仅为未使用的 234/5096 = 1/22,对 GC 的影响就很大了。第三个因为引入了全局变量,也不会产生内存分配,故而和第二个用例消耗性能差不多
4.2 bytes.Buffer
var bufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
var data = make([]byte, 10000)
func BenchmarkBufferWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Write(data)
buf.Reset()
bufferPool.Put(buf)
}
}
func BenchmarkBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
var buf bytes.Buffer
buf.Write(data)
}
}
测试结果如下:
$ go test -bench="^BenchmarkBuffer" -run=none -benchmem
goos: linux
goarch: amd64
pkg: github.com/go-test/benckmark
BenchmarkBufferWithPool-8 8313493 141 ns/op 0 B/op 0 allocs/op
BenchmarkBuffer-8 795733 1321 ns/op 10240 B/op 1 allocs/op
PASS
ok github.com/go-test/benckmark 3.408s
这个例子创建了一个 bytes.Buffer 对象池,而且每次只执行一个简单的 Write 操作,存粹的内存搬运工,耗时几乎可以忽略。而内存分配和回收的耗时占比较多,因此对程序整体的性能影响更大。
5. 在标准库中的应用
5.1 fmt.Printf
Go 语言标准库也大量使用了 sync.Pool,例如 fmt 和 encoding/json。
以下是 fmt.Printf 的源代码(go/src/fmt/print.go):
// go 1.13.6
// pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
type pp struct {
buf buffer
...
}
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
// free saves used pp structs in ppFree; avoids an allocation per invocation.
func (p *pp) free() {
if cap(p.buf) > 64<<10 {
return
}
p.buf = p.buf[:0]
p.arg = nil
p.value = reflect.Value{}
p.wrappedErr = nil
ppFree.Put(p)
}
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
fmt.Printf 的调用是非常频繁的,利用 sync.Pool 复用 pp 对象能够极大地提升性能,减少内存占用,同时降低 GC 压力。
这个例子来源于:深度解密 Go 语言之 sync.Pool