Pool介绍#
总所周知Go 是一个自动垃圾回收的编程语言,采用三色并发标记算法标记对象并回收。如果你想使用 Go 开发一个高性能的应用程序的话,就必须考虑垃圾回收给性能带来的影响。因为Go 在垃圾回收的时候会有一个STW(stop-the-world,程序暂停)的时间,并且如果对象太多,做标记也需要时间。所以如果采用对象池来创建对象,增加对象的重复利用率,使用的时候就不必在堆上重新创建对象可以节省开销。在Go中,golang提供了对象重用的机制,也就是sync.Pool对象池。 sync.Pool是可伸缩的,并发安全的。其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器。 设计的目的是存放已经分配的但是暂时不用的对象,在需要用到的时候直接从pool中取。
任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩。它对外提供了三个方法:New、Get 和 Put。下面用一个简短的例子来说明一下Pool使用:
package main
import (
"fmt"
"sync"
)
var pool *sync.Pool
type Person struct {
Name string
}
func init() {
pool = &sync.Pool{
New: func() interface{} {
fmt.Println("creating a new person")
return new(Person)
},
}
}
func main() {
person := pool.Get().(*Person)
fmt.Println("Get Pool Object1:", person)
person.Name = "first"
pool.Put(person)
fmt.Println("Get Pool Object2:", pool.Get().(*Person))
fmt.Println("Get Pool Object3:", pool.Get().(*Person))
}
结果:
creating a new person
Get Pool Object1: &{}
Get Pool Object2: &{first}
creating a new person
Get Pool Object3: &{}
这里我用了init方法初始化了一个pool,然后get了三次,put了一次到pool中,如果pool中没有对象,那么会调用New函数创建一个新的对象,否则会从put进去的对象中获取。
存储在池中的任何项目都可以随时自动删除,并且不会被通知。Pool可以安全地同时使用多个goroutine。池的目的是缓存已分配但未使用的对象以供以后重用,从而减轻对gc的压力。也就是说,它可以轻松构建高效,线程安全的free列表。但是,它不适用于所有free列表。池的适当使用是管理一组默认共享的临时项,并且可能由包的并发独立客户端重用。池提供了一种在许多客户端上分摊分配开销的方法。很好地使用池的一个例子是fmt包,它维护一个动态大小的临时输出缓冲区存储。底层存储队列在负载下(当许多goroutine正在积极打印时)进行缩放,并在静止时收缩。另一方面,作为短期对象的一部分维护的空闲列表不适合用于池, 因为在该场景中开销不能很好地摊销。 使这些对象实现自己的空闲列表更有效。首次使用后不得复制池。
pool 的两个特点
1、在本地私有池和本地共享池均获取 obj 失败时,则会从其他p偷一个 obj 返回给调用方。
2、obj在池中的生命周期取决于垃圾回收任务的下一次执行时间,并且从池中获取到的值可能是 put 进去的其中一个值,也可能是 newfun处 新生成的一个值,在应用时很容易入坑。
在多个goroutine之间使用同一个pool做到高效,是因为sync.pool为每个P都分配了一个子池,
当执行一个pool的get或者put操作的时候都会先把当前的goroutine固定到某个P的子池上面,
然后再对该子池进行操作。每个子池里面有一个私有对象和共享列表对象,
私有对象是只有对应的P能够访问,因为一个P同一时间只能执行一个goroutine,
【因此对私有对象存取操作是不需要加锁的】。
源码分析
type Pool struct {
// 不允许复制,一个结构体,有一个Lock()方法,嵌入别的结构体中,表示不允许复制
// noCopy对象,拥有一个Lock方法,使得Cond对象在进行go vet扫描的时候,能够被检测到是否被复制
noCopy noCopy
//local 和 localSize 维护一个动态 poolLocal 数组
// 每个固定大小的池, 真实类型是 [P]poolLocal
// 其实就是一个[P]poolLocal 的指针地址
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
// New 是一个回调函数指针,当Get 获取到目标对象为 nil 时,需要调用此处的回调函数用于生成 新的对象
New func() interface{}
}
Pool结构体里面noCopy代表这个结构体是禁止拷贝的,它可以在我们使用 go vet
工具的时候生效;
local是一个poolLocal数组的指针,localSize代表这个数组的大小;同样victim也是一个poolLocal数组的指针,每次垃圾回收的时候,Pool 会把 victim 中的对象移除,然后把 local 的数据给 victim;local和victim的逻辑我们下面会详细介绍到。
New函数是在创建pool的时候设置的,当pool没有缓存对象的时候,会调用New方法生成一个新的对象。
下面我们对照着pool的结构图往下讲,避免找不到北:
// Local per-P Pool appendix.
/*
因为poolLocal中的对象可能会被其他P偷走,
private域保证这个P不会被偷光,至少保留一个对象供自己用。
否则,如果这个P只剩一个对象,被偷走了,
那么当它本身需要对象时又要从别的P偷回来,造成了不必要的开销
*/
type poolLocalInternal struct {
private interface{} // Can be used only by the respective P.
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
/**
cache使用中常见的一个问题是false sharing。
当不同的线程同时读写同一cache line上不同数据时就可能发生false sharing。
false sharing会导致多核处理器上严重的系统性能下降。
字节对齐,避免 false sharing (伪共享)
*/
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
local字段存储的是一个poolLocal数组的指针,poolLocal数组大小是goroutine中P的数量,访问时,P的id对应poolLocal数组下标索引,所以Pool的最大个数runtime.GOMAXPROCS(0)。
通过这样的设计,每个P都有了自己的本地空间,多个 goroutine 使用同一个 Pool 时,减少了竞争,提升了性能。如果对goroutine的P、G、M有疑惑的同学不妨看看这篇文章:The Go scheduler。
poolLocal里面有一个pad数组用来占位用,防止在 cache line 上分配多个 poolLocalInternal从而造成false sharing,cache使用中常见的一个问题是false sharing。当不同的线程同时读写同一cache line上不同数据时就可能发生false sharing。false sharing会导致多核处理器上严重的系统性能下降。具体的可以参考伪共享(False Sharing)。
poolLocalInternal包含两个字段private和shared。
private代表缓存的一个元素,只能由相应的一个 P 存取。因为一个 P 同时只能执行一个 goroutine,所以不会有并发的问题;所以无需加锁
shared则可以由任意的 P 访问,但是只有本地的 P 才能 pushHead/popHead,其它 P 可以 popTail。因为可能有多个goroutine同时操作,所以需要加锁。
type poolChain struct {
// head is the poolDequeue