sync 包中提供了一个线程安全的对象缓冲池,可以降低频繁分配和回收内存对 runtime 的影响。但要注意放入 pool 中的对象的生命周期是不确定的,随时都可能被垃圾回收。sync 也为每个 P(Logical Processor) 建立了独立的缓冲池,降低并发时对锁的争抢。
在使用 sync.Pool
的过程中有个问题需要特别注意,那就是一但有个被大数据撑大的对象放入池中之后,有可能会因为重复的被使用而导致内存不会被释放导致浪费,如果同时出现多个这样的大对象,可能对内存消耗造成不小的负担。解决这个问题有两个思路:
- 限制放入回收池中的对象大小,如标准库中 fmt 包中采取的办法。
// $GOROOT/src/fmt/print.go
func (p *pp) free() {
// 要正确使用sync.Pool,要求每个条目具有大致相同的内存成本
// 若缓存池中存储的类型具有可变大小的缓冲区
// 对放回缓存池的对象增加一个最大缓冲区的硬限制(不能大于65 536字节)
//
// 参见https://golang.org/issue/23199
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)
}
- 按照占用内存大小建立多个缓存池,如标准库 http 包中的代码。
// $GOROOT/src/net/http/h2_bundle.go
var (
http2dataChunkSizeClasses = []int{
1 << 10,
2 << 10,
4 << 10,
8 << 10,
16 << 10,
}
http2dataChunkPools = [...]sync.Pool{
{New: func() interface{} { return make([]byte, 1<<10) }},
{New: func() interface{} { return make([]byte, 2<<10) }},
{New: func() interface{} { return make([]byte, 4<<10) }},
{New: func() interface{} { return make([]byte, 8<<10) }},
{New: func() interface{} { return make([]byte, 16<<10) }},
}
)
func http2getDataBufferChunk(size int64) []byte {
i := 0
for ; i < len(http2dataChunkSizeClasses)-1; i++ {
if size <= int64(http2dataChunkSizeClasses[i]) {
break
}
}
return http2dataChunkPools[i].Get().([]byte)
}
func http2putDataBufferChunk(p []byte) {
for i, n := range http2dataChunkSizeClasses {
if len(p) == n {
http2dataChunkPools[i].Put(p)
return
}
}
panic(fmt.Sprintf("unexpected buffer len=%v", len(p)))
}