临时对象池
当多个goroutine都需要创建同一个对象的时候,如果goroutine过多,可能导致对象的创建数目剧增。而对象又是占用内存的,进而导致的就是内存回收的GC压力徒增。造成“并发大-占用内存大-GC缓慢-处理并发能力降低-并发更大”这样的恶性循环。这时候,我们非常迫切需要有一个对象池,每个gotoutine不再自己单独创建对象,而是从对象池中获取出一个对象(如果池中已经有的话)。这就是sync.Pool出现的目的了
看下面这个例子(取自gomemcache):
// keyBufPool returns []byte buffers for use by PickServer's call to
// crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the
// copies, which at least are bounded in size and small)
var keyBufPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 256)
return &b
},
}
func (ss *ServerList) PickServer(key string) (net.Addr, error) {
ss.mu.RLock()
defer ss.mu.RUnlock()
if len(ss.addrs) == 0 {
return nil, ErrNoServers
}
if len(ss.addrs) == 1 {
return ss.addrs[0], nil
}
bufp := keyBufPool.Get().(*[]byte)
n := copy(*bufp, key)
cs := crc32.ChecksumIEEE((*bufp)[:n])
keyBufPool.Put(bufp)
return ss.addrs[cs%uint32(len(ss.addrs))], nil
}
这是实际项目中的一个例子,这里使用keyBufPool的目的是为了让crc32.ChecksumIEEE所使用的[]bytes数组可以重复使用,减少GC的压力。
但是这里可能会有一个问题,我们没有看到Pool的手动回收函数。 那么是不是就意味着,如果我们的并发量不断增加,这个Pool的体积会不断变大,或者一直维持在很大的范围内呢?
答案是不会的,sync.Pool的回收是有的,它是在系统自动GC的时候,触发pool.go中的poolCleanup函数。
func poolCleanup() {
for i, p := range allPools {
allPools[i] = nil
for i := 0; i < int(p.localSize); i++ {
l := indexLocal(p.local, i)
l.private = nil
for j := range l.shared {
l.shared[j] = nil
}
l.shared = nil
}
p.local = nil
p.localSize = 0
}
allPools = []*Pool{}
}
这个函数会把Pool中所有goroutine创建的对象都进行销毁。
那这里另外一个问题也凸显出来了,很可能我上一步刚往pool中PUT一个对象之后,下一步GC触发,导致pool的GET函数获取不到PUT进去的对象。 这个时候,GET函数就会调用New函数,临时创建出一个对象,并存放到pool中。
根据以上结论,sync.Pool其实不适合用来做持久保存的对象池(比如连接池)。它更适合用来做临时对象池,目的是为了降低GC的压力。
连接池性能测试
package main
import (
"sync"
"testing"
)
var bytePool = sync.Pool{
New: newPool,
}
func newPool() interface{} {
b := make([]byte, 1024)
return &b
}
func BenchmarkAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
obj := make([]byte, 1024)
_ = obj
}
}
func BenchmarkPool(b *testing.B) {
for i := 0; i < b.N; i++ {
obj := bytePool.Get().(*[]byte)
_ = obj
bytePool.Put(obj)
}
}
文件目录下执行 go test -bench .
E:\MyGo\sync>go test -bench .
testing: warning: no tests to run
PASS
BenchmarkAlloc-4 50000000 39.3 ns/op
BenchmarkPool-4 50000000 25.4 ns/op
ok _/E_/MyGo/sync 3.345s
通过性能测试可以清楚地看到,使用连接池消耗的CPU时间远远小于每次手动分配内存。