暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Golang的Sync.Pool详解

johopig 2021-07-05
1842

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来判断的还判断个🐍)


文章转载自johopig,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论