1.为什么要有sync.Pool
创建对象需要申请内存,例如你写了一个网关服务,每个请求过来都创建一个request对象.内存就会越申请越多,当不够的时候,还要gc用来释放不用的对象.
因此,pool就是用来解决频繁申请内存和释放内存(gc)的问题. 注意gc的时候会stw,因此我们要写优雅的程序,减少gc的发生
2.pool流程
pool对外只暴露了两个方法以及一个参数New
get() interface{} :
获取池子中的对象,如果池子为空,就调用New
put(x interface{}):
把使用过的对象归还池子.
New:
当池中没有对象时,get方法将使用New方法来返回一个对象,如果你不设置New方法的话,将会返回nil
3.数据结构
首先pool中
local:指向的是poolLocal的数组的指针,有几个P,就会对应有几个poolLocal,可以认为这是P私有的.存取数据都是先从当前运行的协程关联的P的poolLocal中拿,拿不到,再去别的poolLocal中拿.
那么看下这个本地资源池的结构.一层套一层
type poolLocal struct { poolLocalInternal //这个优化点,防止false share. pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte } 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 poolChain struct { head *poolChainElt tail *poolChainElt } type poolChainElt struct { poolDequeue next, prev *poolChainElt } type poolDequeue struct { headTail uint64 vals []eface }
poolChain是一个动态大小的双端队列池子.
put:
1.先把对象设置该协程关联的P的本地对象池的private上,以便后面取的时候,也可以快速从private取出.private只可以被当前P关联的goroutine访问.P私有的
2.当P关联的本地池的private已经有对象了,那么把这个对象放到公共池(share).这个share可以被任何goroutine访问.
3.注意,由于share是共享的,因此在之前版本,是通过锁来进行读写的.因为多个协程会同时操作.在后来的版本,改造为了一个队列
如果让我们设计一个公共的缓存池,我们可能拿一个map,然后多个线程可以去从这个map中get put.但是,get的时候难免会涉及到锁.
golang里把资源和P进行绑定,每个P下有个private的对象.这个只有与该P绑定的协程才能拿到.拿不到再去本地的公共池拿,而这个公共池也是每个P都有一个,P调度的协程优先从本地header拿,又减少了竞争.相当于把对象资源按照P进行了分片.
这里面还有一些小细节:
在golang 1.13版本之前.公共池是使用的切片.对公共池的读写时需要加锁的.极端情况下,要尝试最多P次抢锁,也获取不到缓存对象,最后得执行New方法返回对象
但是之后版本又进一步优化了.变成了一个双端队列.
4.Get源码
func (p *Pool) Get() interface{} { if race.Enabled { race.Disable() } //1.获取当前协程对应的P,以及P对应的本地池 l, pid := p.pin() //2.获取本地池的private. x := l.private l.private = nil //3.如果本地池的private为空.那么从本地池的共享池中拿(popHead).如果共享池为空,那么从 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 { 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 }
!!!!!这里是扩展,设计到协程的调度原理!!!!! //获取当前P以及对应的本地池 func (p *Pool) pin() (*poolLocal, int) { //获取当前Pid.主要调用到runtime的procPin pid := runtime_procPin() 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.获取当前协程 2.获取协程对应的M 3.系统线程在对协程调度的时候,有时候会抢占当前正在执行的协程的所属p,原因是不能让某个协程一直占用计算资源,那么在进行抢占的时候会判断m是否适合抢占,其中有一个条件就是判断m.locks==0,看对mp.locks++,就是禁止当前P被抢占 同理 procUnpin就是-- 4.返回P的id func procPin() int { _g_ := getg() mp := _g_.m mp.locks++ return int(mp.p.ptr().id) }
//G--M type g struct { m *m // current m; offset known to arm liblink } //M--P type m struct { p puintptr // attached p for executing go code (nil if not executing go code) }
以上代码很简单
- 1.本地池的private
- 2.本地池的共享池
- 3.getSlow
- 4.New
getSlow这个操作是啥呢?我们猜测肯定是从别的P对应的共享池去拿了.看源码
func (p *Pool) getSlow(pid int) interface{} { // See the comment in pin regarding ordering of the loads. size := atomic.LoadUintptr(&p.localSize) // load-acquire locals := p.local // load-consume // Try to steal one element from other procs. //1.尝试从别的P下偷一个资源对象 for i := 0; i < int(size); i++ { l := indexLocal(locals, (pid+i+1)%int(size)) //这里是从尾部拿的,如果是P对应的协程是从头部拿 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 } 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 }
从尾部拿资源对象
victim和victimSize
每次垃圾回收的时候,Pool 会把 victim 中的对象移除,然后把 local 的赋值给victim.
实践
/** pool初衷是为了避免创建大量对象,占用内存,导致gc,gc时又要清除对象 因此,在gc开始前,会把pool中的对象清除,但是在验证时,发现并没有.看源码.当gc时,会把victim清除,之后把local赋给victim. get的时候,也是先从local获取,如果没有,再去victim获取. */ func Test_gc(t *testing.T) { pl := sync.Pool{ New: func() interface{} { return "hello" }, } pl.Put("a") pl.Put("b") pl.Put("c") fmt.Println(pl.Get()) runtime.GC() fmt.Println("第一次gc过后" + pl.Get().(string)) runtime.GC() fmt.Println("第二次gc过后" + pl.Get().(string)) }
好思想啊.不过这个不是go开创的.
5.PUT操作
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() } l, _ := p.pin() if l.private == nil { //1.放到本地池的private中,同时将x变量置空 l.private = x x = nil } if x != nil { //2.如果x不为空,就是说没有放到private了,那么把对象放到shared l.shared.pushHead(x) } runtime_procUnpin() if race.Enabled { race.Enable() } }
6.池中对象的清理
什么时候会销毁池子中的对象呢?毕竟池子中的对象也会gc的.
答案是gc之前.
sync包在初始化的时候,会注册一个清理函数
func init() { runtime_registerPoolCleanup(poolCleanup) } // Implemented in runtime. func runtime_registerPoolCleanup(cleanup func())
那么清理函数的逻辑是什么?
3.13之前
问题点:
-
每次GC都回收所有对象,如果缓存对象数量太大,会导致STW1阶段的耗时增加。
-
每次GC都回收所有对象,导致缓存对象命中率下降,New方法的执行造成额外的内存分配消耗。
func poolCleanup() {
// ...
for i, p := range allPools {
// 有多少个Sync.Pool对象,遍历多少次
allPools[i] = nil
for i := 0; i < int(p.localSize); i++ {
// 有多少个P,遍历多少次
l := indexLocal(p.local, i)
l.private = nil
for j := range l.shared {
// 清空shared区中每个缓存对象
l.shared[j] = nil
}
l.shared = nil
}
// ...
}
// ...
}
这里对每个池子中的对象都赋值为nil,直接清理.
扩展下,啥时候gc,gc会发生什么?
3.14之后
func poolCleanup() { // This function is called with the world stopped, at the beginning of a garbage collection. // It must not allocate and probably should not call any runtime functions. // Because the world is stopped, no pool user can be in a // pinned section (in effect, this has all Ps pinned). // Drop victim caches from all pools. for _, p := range oldPools { p.victim = nil p.victimSize = 0 } // Move primary cache to victim cache. for _, p := range allPools { p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } // The pools with non-empty primary caches now have non-empty // victim caches and no pools have primary caches. oldPools, allPools = allPools, nil }
注意里面有个allPools. oldPools
var ( allPoolsMu Mutex // allPools is the set of pools that have non-empty primary // caches. Protected by either 1) allPoolsMu and pinning or 2) // STW. allPools []*Pool // oldPools is the set of pools that may have non-empty victim // caches. Protected by STW. oldPools []*Pool )
这个allPools是这个包的公共变量.是一个切片,保存了所有使用的池子.在开始使用pool的时候,就会把Pool对象加进来.
主要是将 local 和 victim 作交换,那么不至于GC 把所有的 Pool 都清空了,而是需要两个 GC
周期才会被释放.从上面的例子也可以看到,在两次gc后.只能New了
总结:
1.pool是为了减少大量创建对象,频繁gc.同时也要考虑,池子中的对象什么时候回收,有可能有很多不用的对象,因此在低版本中,每次gc会清理.
2.pool中将资源对象进行分片存储,其实类似concurrentHashMap.将对象分在不同的桶中. 同时,每个P也会有私有的private.加快get
3.无锁环形链表的使用.避免一些加锁的操作.
本来在看fasthttp的源码,发下里面大量使用了池子.读pool的源码,真的感觉设计巧妙!! 以下是参考的博客.写的真心很棒
参考:
https://zhuanlan.zhihu.com/p/99710992?utm_source=wechat_timeline 深度好文