Go sync.Pool

Go 提供了sync.Pool对象池,用于复用高频创建析构的对象以减少内存申请造成的性能问题。

基本用法

func main() {
    type myStruct struct {
        a, b int
    }
    myPool := sync.Pool{
        New: func() interface{} {
            return new(myStruct)
        },
    }
    val1 := myPool.Get().(*myStruct)
    val1.a = 2
    fmt.Println(*val1)
    myPool.Put(val1)
}

注意点

如上所示,只需要简单几行就可以实现定制的对象池了,但是有很多注意点:

  • 只能保存指针,以便用interface{}传递,如果用对象会造成大量拷贝耗时,得不偿失
  • 一定要实现sync.Pool.New函数,该函数用于在没有池中对象时 Get 时新建对象,如果不实现则始终返回 nil
  • Pool 中保存的对象在 GC 时会被析构,所以不能依赖于池中已经存在的对象;同时,由于 GC 的存在,也不用担心 Pool 导致内存过大的问题
  • 一个自定义 Pool 对象最好以 Singleton 模式运行,因为 Pool 底层实现涉及一个全局锁,如果频繁创建、析构会造成性能瓶颈

实现原理

  • init 函数中注册了GC回调函数,每次执行 GC 时 Pool 中的对象都会被回收
// src/pkg/sync/pool.go
func init() {
    runtime_registerPoolCleanup(poolCleanup)
}
  • 为了应对大量 goroutine 间的竞争,Pool 为每个 P 分配了一个子 Pool,这样来避免发生竞争。 每个子池里面有一个私有对象共享对象列表,私有对象是只有对应的 P 能够访问, 因为一个 P 同一时间只能执行一个 goroutine,因此对私有对象存取操作是不需要加锁的。 共享列表是和其他 P 分享的,因此操作共享列表是需要加锁的。

  • 获取对象过程是:

    1. 固定到某个 P,尝试从私有对象获取,如果私有对象非空则返回该对象,并把私有对象置空;
    2. 如果私有对象是空的时候,就去当前子池的共享列表获取(需要加锁);
    3. 如果当前子池的共享列表也是空的,那么就尝试去其他 P 的子池的共享列表偷取一个(需要加锁);
    4. 如果其他子池都是空的,最后就用用户指定的 New 函数产生一个新的对象返回。

可以看到经过上面的二级缓存设计,一次 get 操作最少 0 次加锁,最大 N(N 等于 MAXPROCS)次加锁。

  • 归还对象的过程:
    1. 固定到某个 P,如果私有对象为空则放到私有对象;
    2. 否则加入到该 P 子池的共享列表中(需要加锁)。

可以看到一次 put 操作最少 0 次加锁,最多 1 次加锁。

  • 关于全局锁 在创建 Pool 时,为了全局共享,会把 Pool 放入一个全局切片,这时会获取一个全局锁。 如果大并发创建 Pool,会发生非常严重的性能瓶颈。

改进

  • 可以在sync.Pool外封装一层,实现对象的reset操作,避免垃圾数据造成问题
type Reseter interface {
    Reset()
}

type MyPool struct {
    pool sync.Pool
}

func (p *MyPool) Get() interface{} {
    return p.pool.Get()
}

func (p *MyPool) Put(x interface{}) {
    x.(Reseter).Reset()
    p.pool.Put(x)
}

func NewMyPool(f func() interface{}) *MyPool {
    return &MyPool{
        pool: syncPool {
            New: f,
        },
    }
}

// 下面是具体结构

type MyStruct struct {
    A, b int
}
func (p *MyStruct) Reset() {
    p.A, p.b =, 0, 0
}

func main() {
    pool := NewMyPool(func() interface{} {
        return &MyStruct{}
    })
    a := pool.Get()
    fmt.Println(a)
    pool.Put(a)
}

其他

  • sync.Pool之前,一般用带缓存的 channel 实现特定对象的缓存。 由于 channel 的底层依赖 Mutex,所以当并发较大时会有性能问题,所以官方提供了 Pool。