7分钟读懂Go的临时对象池pool以及其应用场景
临时对象池pool是啥?
sync.Pool给了一大段注释来说明pool是啥,我们看看这段都说了些什么。
临时对象池是一些可以分别存储和取出的临时对象。
池中的对象会在没有任何通知的情况下被移出(释放或者重新取出使用)。如果pool中持有某个对象的唯一引用,则该对象很可能会被回收。
Pool在多goroutine使用环境中是安全的。
Pool是用来缓存已经申请了的目前未使用的接下来可能会使用的内存,以此缓解GC压力。使用它可以方便高效的构建线程安全的freelist(一种用于动态内存申请的数据结构)。然而,它并不适合所有场景的freelist。
在同一package中独立运行的多个独立线程之间静默共享一组临时元素才是pool的合理使用场景。Pool提供在多个独立client之间共享临时元素的机制。
在fmt包中有一个使用Pool的例子,它维护了一个动态大小的输出buffer。
另外,一些短生命周期的对象不适合使用pool来维护,这种情况下使用pool不划算。这是应该使用它们自己的freelist(这里可能指的是go内存模型中用于缓存<32k小对象的freelist)更高效。
Pool一旦使用,不能被复制。
Pool结构体的定义为:
typePoolstruct{
noCopynoCopy
localunsafe.Pointer//本地P缓存池指针
localSizeuintptr//本地P缓存池大小
//当池中没有可能对象时
//会调用New函数构造构造一个对象
Newfunc()interface{}
}
Pool中有两个定义的公共方法,分别是Put-向池中添加元素;Get-从池中获取元素,如果没有,则调用New生成元素,如果New未设置,则返回nil。
Get
Pool会为每个P维护一个本地池,P的本地池分为私有池private和共享池shared。私有池中的元素只能本地P使用,共享池中的元素可能会被其他P偷走,所以使用私有池private时不用加锁,而使用共享池shared时需加锁。
Get会优先查找本地private,再查找本地shared,最后查找其他P的shared,如果以上全部没有可用元素,最后会调用New函数获取新元素。
func(p*Pool)Get()interface{}{
ifrace.Enabled{
race.Disable()
}
//获取本地P的poolLocal对象
l:=p.pin()
//先获取private池中的对象(只有一个)
x:=l.private
l.private=nil
runtime_procUnpin()
ifx==nil{
//查找本地shared池,
//本地shared可能会被其他P访问
//需要加锁
l.Lock()
last:=len(l.shared)-1
iflast>=0{
x=l.shared[last]
l.shared=l.shared[:last]
}
l.Unlock()
//查找其他P的shared池
ifx==nil{
x=p.getSlow()
}
}
ifrace.Enabled{
race.Enable()
ifx!=nil{
race.Acquire(poolRaceAddr(x))
}
}
//未找到可用元素,调用New生成
ifx==nil&&p.New!=nil{
x=p.New()
}
returnx
}
getSlow,从其他P中的shared池中获取可用元素:
func(p*Pool)getSlow()(xinterface{}){
//Seethecommentinpinregardingorderingoftheloads.
size:=atomic.LoadUintptr(&p.localSize)//load-acquire
local:=p.local//load-consume
//Trytostealoneelementfromotherprocs.
pid:=runtime_procPin()
runtime_procUnpin()
fori:=0;i=0{
x=l.shared[last]
l.shared=l.shared[:last]
l.Unlock()
break
}
l.Unlock()
}
returnx
}
Put
Put优先把元素放在private池中;如果private不为空,则放在shared池中。有趣的是,在入池之前,该元素有1/4可能被丢掉。
func(p*Pool)Put(xinterface{}){
ifx==nil{
return
}
ifrace.Enabled{
iffastrand()%4==0{
//随机把元素扔掉...
//Randomlydropxonfloor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
l:=p.pin()
ifl.private==nil{
l.private=x
x=nil
}
runtime_procUnpin()
ifx!=nil{
//共享池访问,需要加锁
l.Lock()
l.shared=append(l.shared,x)
l.Unlock()
}
ifrace.Enabled{
race.Enable()
}
}
poolCleanup
当世界暂停,垃圾回收将要开始时,poolCleanup会被调用。该函数内不能分配内存且不能调用任何运行时函数。原因:
防止错误的保留整个Pool
如果GC发生时,某个goroutine正在访问l.shared,整个Pool将会保留,下次执行时将会有双倍内存
funcpoolCleanup(){
fori,p:=rangeallPools{
allPools[i]=nil
fori:=0;i
案例1:gin中的Contextpool
在web应用中,后台在处理用户的每条请求时都会为当前请求创建一个上下文环境Context,用于存储请求信息及相应信息等。Context满足长生命周期的特点,且用户请求也是属于并发环境,所以对于线程安全的Pool非常适合用来维护Context的临时对象池。
Gin在结构体Engine中定义了一个pool:
typeEnginestruct{
//...省略了其他字段
poolsync.Pool
}
初始化engine时定义了pool的New函数:
engine.pool.New=func()interface{}{
returnengine.allocateContext()
}
//allocateContext
func(engine*Engine)allocateContext()*Context{
//构造新的上下文对象
return&Context{engine:engine}
}
ServeHttp:
//从pool中获取,并转化为*Context
c:=engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request=req
c.reset()//reset
engine.handleHTTPRequest(c)
//再扔回pool中
engine.pool.Put(c)
案例2:fmt中的printerpool
printer也符合长生命周期的特点,同时也会可能会在多goroutine中使用,所以也适合使用pool来维护。
printer与它的临时对象池
//pp用来维护printer的状态
//它通过sync.Pool来重用,避免申请内存
typeppstruct{
//...字段已省略
}
varppFree=sync.Pool{
New:func()interface{}{returnnew(pp)},
}
获取与释放:
funcnewPrinter()*pp{
p:=ppFree.Get().(*pp)
p.panicking=false
p.erroring=false
p.fmt.init(&p.buf)
returnp
}
func(p*pp)free(){
p.buf=p.buf[:0]
p.arg=nil
p.value=reflect.Value{}
ppFree.Put(p)
}
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。