Sync.Pool
GMP模型
这篇文章不是讲GMP的,只不过想要理解Sync.Pool的话必须对GMP有了解,所以下面会简单提一下。。
Go中调度器是GMP模型,简单理解G就是goroutine;M可以类比内核线程,是执行G的地方;P是调度G以及为G的执行准备所需资源。一般情况下,P的数量等于CPU的可用核心数,也可由runtime.GOMAXPROCS指定
Go有这样的调度规则:某个G不能一直占用M(否则GC STW的时候需要一直等下去),在某个时刻,runtime会判断当前M是否可以被抢占,即M上正在执行的G让出,P在合理的时刻将G调度到合理的M上执行(参见symon)。在runtime里面,每个P维护一个本地存放待执行G的队列localq,同时还存在一个全局的待执行G的队列globalq;调度就是P从localq或globalq中取出G到对应的M上执行。
所谓抢占调度,即runtime将G抢占移出运行状态,拷贝G的执行栈放入待执行队列中,可能是某个P的localq,也可能是globalq,等待下一次调度,因此当被抢占的G重回待执行队列时有可能此时的P与前一次运行的P并非同一个。
在抢占式情况下,如果一个goroutine运行时间过长,它就会被剥夺运行权。
Sync.Pool使用场景及目的
在高并发场景下,频繁的new对象可能会导致频繁的GC,为了减少GC的压力,这时候就可以使用sync.Pool对象池。
Sync.Pool是并发安全的,设计的目的是存放已经分配的但是暂时不用的对象,在需要用到的时候直接从pool中取。
源码解析
Pool{}
// A Pool must not be copied after first use.
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{}
}
第 5
行表明local
其实是一个指向poolLocal
数组的指针,poolLocal
数组的个数等于P(默认等于CPU核数)的个数。
❝这里的P非Pool的缩写,而是go的GMP模型中的P。「每一个运行的M都必须绑定一个P」,就像线程必须在一个CPU核上执行一样,由P来调度G在M上的运行,M的个数和P的个数不一定一样多(会有休眠的M或者不需要太多的M)(最大10000)。
M
线程想运行任务就得绑定P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。需要明白的一点是,一个G可能会被不同的P调度,例如当G阻塞了,P会暂停对它的调度并放入队列,然后会重新取其他G进行操作,等到阻塞结束想回来操作G的时候,这个G可能已经被其他M偷走了。。。
❞
第 6
行的localSize
表示poolLocal
数组长度,后面会通过它以及当前P的序号取出指定的poolLocal
poolLocal{}
// 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
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
第
8
行定义了一个内嵌结构体poolLocalInternal
,其中有字段private
和shared
,从注解可以看出一个是私有对象,「只能被当前P访问」;一个是共享列表对象,可以被任何P访问。因为同一时刻一个P只能调度一个G,所以无需加锁,但是对共享列表对象进行操作时,因为可能有多个goroutine同时操作,所以需要加锁(pophead方法里)。第
12
行是用来防止内存伪共享的,目前还没有去了解。。。(不过不影响对下面文章的阅读)
Pin()
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin() //该方法的作用是获取当前P,这样就和当前G绑定起来了(关联到GMP模型中的抢占调度),意味着当前P一直执行当前G
s := atomic.LoadUintptr(&p.localSize)// poolLocal数组长度
l := p.local // poolLocal首地址
if uintptr(pid) < s { // pid作为poolLocal数组下标,所以必须小于数组长度
return indexLocal(l, pid), pid
}
return p.pinSlow()
}
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
return (*poolLocal)(lp)
}
这个方法是用来获取当前P的poolLocal
❝获取poolLocal的时候,会根据下标pid获取对应的poolLocal。但这不是总是成功的,当修改了GOMAXPROCS导致P的个数大于poolLocal的个数时候会重建poolLocal数组,所以有时就需要使用pinSlow()方法获取新的poolLocal
❞
Get()
func (p *Pool) Get() interface{} {
if race.Enabled { //查看是否启用数据竞争检测,启用的话会打印一堆东西出来,通过go run -race main.go查看
race.Disable()
}
l, pid := p.pin()
x := l.private
l.private = nil
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() // pin()里设置了禁止抢占,直到这里才解除,
// 因为只允许当前特定P操作自己的poolLoacl里的私有对象
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.
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
}
}
略......
}
第 5
行代码根据当前P取到对应的poolLocal第 6、7
行从私有对象取值,然后把私有对象置空第 12
行如果私有对象为空,就去共享对象列表取数据,并从共享列表中删除该值第 13
行共享对象列表还是没数据,则会从其他P的poolLocal
里的共享池中偷一个过来getSlow(pid)
,并删除被偷P的共享池该值第 25
行共享对象列表还是没数据,则通过一开始注册的New()
方法获取值返回值
❝为什么取出来之后原来的对象都要置空呢?因为Pool.Get()只会返回2种值:一种是按你注册的new方法取出来给你;另外一种则是你之前操作了之后放回来的值,而Pool.Put()并不关心你放什么东西进来,如果你不置空,那可能每次都返回同样的值,但你并不能保证你取出来之后不对它做修改,所以取出来后原对象都要置空,置空了那边才可以放回去,至于你放回去的时候有没有修改这个值,Pool并不关心(当然,不能放个nil的进来,不然后面那些逻辑是根据nil来判断的还判断个🐍)
❞




