sync.Pool可以被看做是存放可被重用的值的容器,这个容器具有以下特性:可自动伸缩、高效、并发安全。 因为它的使用场景并不适用于诸如数据库连接池这类需要我们自己管理生命周期的资源对象的池化场景,所以一般把sync.Pool称为临时对象池(其实叫做临时对象缓存更合适),主要用来存放已经分配的但是暂时不需要使用的对象,在需要使用的时候再从临时对象池中取出。

1.sync.Pool的使用

 1pool := &sync.Pool{
 2	New: func() interface{} {
 3		return 1
 4	},
 5}
 6v := pool.Get().(int)
 7fmt.Println(v)
 8pool.Put(2)
 9v1, _ := pool.Get().(int)
10fmt.Println(v1)
11fmt.Println(pool.Get())
  • sync.Pool需要一个公有字段New,这个字段是一个函数类型,当临时对象池中无可用的对象值时会被调用,用来创建对象值。
  • sync.Pool有一个Get()方法,用于从临时对象池中获取一个对象值,如果Get方法返回的对象值是池中,在返回时会把这个对象值从池中删除
  • sync.Pool还有一个Put()方法,用于将一个对象值放到临时对象池中

2.深入sync.Pool

通过sync.Pool的源码https://golang.org/src/sync/pool.go可以看出:

 1// A Pool must not be copied after first use.
 2type Pool struct {
 3	noCopy noCopy
 4
 5	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
 6	localSize uintptr        // size of the local array
 7
 8	// New optionally specifies a function to generate
 9	// a value when Get would otherwise return nil.
10	// It may not be changed concurrently with calls to Get.
11	New func() interface{}
12}
13......

为了实现多个goroutine操作同一个sync.Pool时的高效性,sync.Pool为每个P(Processor)各分配一个本地池,当调用Get或Put方法时,会将从当前goroutine关联P的本地池中获取或存放对象。 每个P的本地池范围私有对象和共享列表对象,私有对象只有其所属的P能访问,共享列表可以被任务P访问。因为从Go语言的并发调度模型可知,同一时刻一个P只能执行一个goroutine,所以一个P的私有对象是协程安全的,访问时不需要加锁,而共享列表可以被多个P访问,是协程不安全的,所以访问时需要加锁。

go-sync-pool.jpg

2.1 获取对象

当我们从sync.Pool中获取对象时,会首先从当前goroutine的P的本地池的私有对象获取,如果私有对象不存在则会从当前P的共享列中获取,如果当前P的共享列表也是空的,则就会从其他P的共享列表获取。如果所有P的本地池都是空的,则直接使用New字段指定的函数创建一个新的对象返回。

2.2 放回对象

当我们往sync.Pool中放置对象时,如果当前P的私有对象不存在,则将其保存为当前P的私有对象,如果当前P的私有对象已经存在,则将其放置到当前P的共享列表中。

2.3 sync.Pool和GC

每次GC会清除掉sync.Pool中缓存的对象,因此对象缓存的有效期是到下一次GC之前。

 1pool := &sync.Pool{
 2	New: func() interface{} {
 3		return 1
 4	},
 5}
 6v := pool.Get().(int)
 7fmt.Println(v)
 8pool.Put(2)
 9runtime.GC()
10v1, _ := pool.Get().(int)
11fmt.Println(v1)

3.总结

sync.Pool适用于存放已经分配的但是暂时不需要使用的对象,降低了复杂对象的创建和GC代价。 sync.Pool本身是协程安全的,访问时由锁开销。每一次Get操作最少0次加锁,最大NMAXPROCS次加锁,每一次Put操作最少0次加锁,最多1次加锁。sync.Pool中的对象受GC影响,因此sync.Pool不适用于连接池的场景。