官方对对象池的定义
// A Pool is a set of temporary objects that may be individually saved andretrieved.
//
// Any item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.
//
// A Pool is safe for use by multiple goroutines simultaneously.
//
// Pool's purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector. That is, it makes it easy to build efficient, thread-safe free lists. However, it is not suitable for all free lists
官方对对象池使用场景的描述
// An appropriate use of a Pool is to manage a group of temporary items
// silently shared among and potentially reused by concurrent independent clients of a package. Pool provides a way to amortize allocation overhead across many clients.
//
// On the other hand, a free list maintained as part of a short-lived object is not a suitable use for a Pool, since the overhead does not amortize well in that scenario. It is more efficient to have such objects implement their own free list.
//
// A Pool must not be copied after first use.
我个人的理解pool就是一个可以回收资源的对象的地方,可以避免对同一个对象重复的创建销毁,从而来节省开销。(但当维护pool本身的开销大于创建销毁对象的开销时,pool就不实用了。)
pool的结构
/*
noCopy 防止copy(一个对象池在首次使用以后就不允许copy了)
//防止方法就是一旦检测到存在这个nocopy字段就时不允许copy了
local 本地对象池
localSize 本地对象池的大小
New 生成对象的接口方法
*/
type Pool struct {
noCopy noCopy
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 func() interface{}
}
// Local per-P Pool appendix.
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 //每个p对应的pool
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
//防止“false sharing/伪共享”
/*
缓存系统中是以缓存行为单位存储的。
缓存行通常是 64 字节,当缓存行加载其中1个字节时候,其他的63个也会被加载出来,
加锁的话也会加锁整个缓存行,当下图所示x、y变量都在一个缓存行的时候,
当进行X加锁的时候,正好另一个独立线程要操作Y,这会儿Y就要等X了,此时就不无法并发了。
*/
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
pin
在介绍get/put前,关键的基础函数pin需要先了解一下。 一句话说明用处:确定当前P绑定的localPool对象 (这里的P,是MPG中的P)
MPG是GO中调度器的模型 实际上包括四个结构
G:goroutine,每个Goroutine对应一个G结构体,存储运行堆栈,状态,以及任务函数
P:processer,对于G来说相当于逻辑的处理器,G只有获得了p才能运行,对M来说,p是相关运行环境和上下文,(内存分配状态,任务队列)
M:Machine:对应着OS内核级线程,代表着真正执行计算的资源,再绑定有效的p后,进入schedule循环。M的数量是不定的,由Go runtime调整。
S:sched:Go的调度器,维护由M和G的队列以及调度器的一些状态信息。
// pin pins the current goroutine to P, disables preemption and
// returns poolLocal pool for the P and the P's id.
// Caller must call runtime_procUnpin() when done with the pool.
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin()
// In pinSlow we store to local and then to localSize, here we load in opposite order.
// Since we've disabled preemption, GC cannot happen in between.
// Thus here we must observe local at least as large localSize.
// We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).
s := atomic.LoadUintptr(&p.localSize) // load-acquire
l := p.local // load-consume
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
return p.pinSlow()
}
1.12之前的GETPUT操作可以总结为下图,在后面的1.13去掉了shared锁,将shared变成双端队列,并引入了受害者队列
sync.Pool workflow in Go 1.12
PUT
优先放入private空间,后面再放入shared空间
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
if race.Enabled {
if fastrand()%4 == 0 {
// Randomly drop x on floor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
// 获取当前的poolLocal
l, _ := p.pin()
//如果private为nil,则优先进行设置,并标记x
if l.private == nil {
l.private = x
x = nil
}
//
// 如果标记x不为nil,则将x设置到shared队列头中
if x != nil {
l.shared.pushHead(x)
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
GET
优先从private空间拿,再加锁从shared空间拿,还没有再从其他的PoolLocal的shared空间拿,还没有就直接new一个返回
func (p *Pool) Get() interface{} {
if race.Enabled {
race.Disable()
}
// 获取当前的poolLocal
l, pid := p.pin()
// 从private中获取
x := l.private
l.private = nil
// 不存在,则继续从shared空间拿
if x == nil {
// Try to pop the head of the local shard. We prefer
// the head over the tail for temporal locality of
// reuse.
//在自己空间的共享队列上,就从头去拿
x, _ = l.shared.popHead()
if x == nil {
//如果没有,从其他P的pool中拿
x = p.getSlow(pid)
}
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
if x == nil && p.New != nil {
x = p.New()
}
return x
}
func (p *Pool) getSlow(pid int) interface{} {
// See the comment in pin regarding ordering of the loads.
// 获取poolLocal数组的大小
size := atomic.LoadUintptr(&p.localSize) // load-acquire
locals := p.local // load-consume
// Try to steal one element from other procs.
//从其他队列队尾拿
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i+1)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// Try the victim cache. We do this after attempting to steal
// from all primary caches because we want objects in the
// victim cache to age out if at all possible.
size = atomic.LoadUintptr(&p.victimSize)
if uintptr(pid) >= size {
return nil
}
//从victim队列里面拿
locals = p.victim
l := indexLocal(locals, pid)
if x := l.private; x != nil {
l.private = nil
return x
}
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// Mark the victim cache as empty for future gets don't bother
// with it.
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
Go 1.13 版将 shared
用一个双向链表poolChain
作为储存结构,这次改动删除了锁并改善了 shared
的访问。以下是 shared
访问的新流程:
每个处理器可以在其 shared 队列的头部 push 和 pop,而其他处理器访问 shared 只能从尾部 pop。由于 next/prev 属性,shared 队列的头部可以通过分配一个两倍大的新结构来扩容,该结构将链接到前一个结构。初始结构的默认大小为 8。这意味着第二个结构将是 16,第三个结构 32,依此类推。
新引进的victim 缓存关于引入 victim 缓存的 commit,所谓受害者缓存 Victim Cache,、容量很小的全相联缓存。当一个数据块被逐出缓存时,并不直接丢弃,而是暂先进入受害者缓存。如果受害者缓存已满,就替换掉其中一项。当进行缓存标签匹配时,在与索引指向标签匹配的同时,并行查看受害者缓存,如果在受害者缓存发现匹配,就将其此数据块与缓存中的不匹配数据块做交换,同时返回给处理器。),新策略非常简单。现在有两组池:活动池和存档池allPools
和 oldPools
。当 GC 运行时,它会将每个池的引用保存到池中的(victim),然后在清理当前池之前将该组池变成存档池
(为了保证GC后pool还有对象可用)
既让对象至少存活两个 GC 区间。
// 从所有 pool 中删除 victim 缓存
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// 把主缓存移到 victim 缓存
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
// 非空主缓存的池现在具有非空的 victim 缓存,并且池的主缓存被清除
oldPools, allPools = allPools, nil
应用程序现在将有一个循环的 GCo 来 创建 / 收集 具有备份的新元素,这要归功于 victim 缓存。在之前的流程图中,将在请求 "shared" pool 的流程之后请求 victim 缓存。
pool的生命周期
看一下sync/pool.go文件会给我们展示一个初始化函数,这个函数里面的内容:
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
func gcStart(trigger gcTrigger) {
[...]
// clearpools before we start the GC
clearpools()
当调用垃圾回收时,性能会下降。pools在每次垃圾回收启动时都会被清理。这个文档其实已经有警告我们.
所以pool的生命周期其实是在两次GC之间。
网友评论