sync 包中提供了一个线程安全的对象缓冲池,可以降低频繁分配和回收内存对 runtime 的影响。但要注意放入 pool 中的对象的生命周期是不确定的,随时都可能被垃圾回收。sync 也为每个 P(Logical Processor) 建立了独立的缓冲池,降低并发时对锁的争抢。

在使用 sync.Pool 的过程中有个问题需要特别注意,那就是一但有个被大数据撑大的对象放入池中之后,有可能会因为重复的被使用而导致内存不会被释放导致浪费,如果同时出现多个这样的大对象,可能对内存消耗造成不小的负担。解决这个问题有两个思路:

  1. 限制放入回收池中的对象大小,如标准库中 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)
}
  1. 按照占用内存大小建立多个缓存池,如标准库 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)))
}