[golang]sync.Pool源码(PMG,false sharing,victim cache,环形链表)

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之前

   问题点:

  1. 每次GC都回收所有对象,如果缓存对象数量太大,会导致STW1阶段的耗时增加。

  2. 每次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  深度好文

https://mp.weixin.qq.com/s?__biz=MzA4ODg0NDkzOA==&mid=2247487149&idx=1&sn=f38f2d72fd7112e19e97d5a2cd304430&source=41#wechat_redirect 直击心灵

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值