Go 源码阅读:sync.Pool

sync.Pool管理一组可以单独保存和检索的临时对象。目的是缓存已分配但未使用的对象,以供以后重用,从而减轻GC的压力。核心就是PutGetNew

sync.Pool的结构体

使用sync.Pool 需要提供一个New方法,以便在池中没有缓存的对象时,调用New创建

type Pool struct {
	noCopy noCopy // 静态检查机制:内置noCopy结构体的对象在第一次使用后不会再发生复制

	local     unsafe.Pointer // local 固定大小 per-P 池, 实际类型为[P]poolLocal
	localSize uintptr        // local array 的大小

	victim     unsafe.Pointer // 在上一个GC周期local被poolCleanup函数放置于此,它可能尚未被清理。     后面再讲
	victimSize uintptr        // victims array 的大小

	// 在Get方法失败的情况下,选择性的创建一个值
	New func() interface{}
}

poolLocal结构体

type poolLocalInternal struct {
	private interface{} // 只能被各自的P使用
	shared  poolChain   // 可以被任意P使用
}

type poolLocal struct {
	poolLocalInternal

    // 对齐到机器的缓存行大小,以避免false sharing  [1]
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
  • private 只保存一个对象,且只能被拥有当前poolLocalP 访问
  • shared 是一个链表,可以被其他P访问

单个sync.Pool在运行时的内存状态

image-20221201122101843Get

有了上面这张图,Get如何从池中获取对象,也能猜个七七八八了。

  1. 优先选择当前P对应的poolLocal.private
  2. 若取不到,优先从当前Psharehead链表的头部取出一个对象。
  3. 若取不到,则尝试从其他线程中sharetail中steal一个对象。
  4. 若取不到,则调用New创建一个对象。
func (p *Pool) Get() interface{} {
	// 如果启用的 race 检查则先停用
	if race.Enabled {
		race.Disable()
	}
	// 返回pid,和 poolLocal
	l, pid := p.pin()
	// 尝试从private中获取数据
	x := l.private
	// 获取之后将private置nil,相当于从poolLocal中移除对象
	l.private = nil
	// 若从private中获取失败
	if x == nil {
		// 为了更好的利用时间局部性,从 shared 头部读取对象
		x, _ = l.shared.popHead()
		// 如果读取不到,则steal获取新的缓存对象
		if x == nil {
			x = p.getSlow(pid)
		}
	}
	runtime_procUnpin()
	// 恢复 race 检查
	if race.Enabled {
		race.Enable()
		if x != nil {
			race.Acquire(poolRaceAddr(x))
		}
	}
	// 若还是取不出来则调用New 创建
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

窃取对象

窃取的策略

  1. 尝试从其他p的shared窃取。
  2. 若取不到,尝试从上一轮GC时被放置(惰性回收)的poolLocalprivate取对象。
  3. 若取不到,尝试从上一轮GC时被放置(惰性回收)的poolLocalshared取对象。
  4. 还是取不到,返回nil,有Get函数调用New创建一个对象。
func (p *Pool) getSlow(pid int) interface{} {
	// 遍历所有p的 poolLocal 尝试从shared中窃取一个对象
	size := runtime_LoadAcquintptr(&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
		}
	}
	
	// 遍历所有p的victim
	size = atomic.LoadUintptr(&p.victimSize)
	if uintptr(pid) >= size {
		return nil
	}
	// 遍历当前p的victim的private
	locals = p.victim
	l := indexLocal(locals, pid)
	if x := l.private; x != nil {
		l.private = nil
		return x
	}
	// 遍历其他p的victim的shared
	for i := 0; i < int(size); i++ {
		l := indexLocal(locals, (pid+i)%int(size))
		if x, _ := l.shared.popTail(); x != nil {
			return x
		}
	}
	atomic.StoreUintptr(&p.victimSize, 0)

	return nil
}

Put

将一个(不确定对象状态)的对象放入到池中,遵循以下策略。

  1. 优先放入private
  2. 如果private已经有值,则尝试放入shared
func (p *Pool) Put(x interface{}) {
   if x == nil {
      return
   }
   // 停用 race
   if race.Enabled {
      if fastrand()%4 == 0 {
         // Randomly drop x on floor.
         return
      }
      race.ReleaseMerge(poolRaceAddr(x))
      race.Disable()
   }
   // 获取 localPool
   l, _ := p.pin()
   // 优先放入 private
   if l.private == nil {
      l.private = x
      x = nil
   }
   // 如果不能放入 private 则放入 shared
   if x != nil {
      l.shared.pushHead(x)
   }
   runtime_procUnpin()

   // 恢复race
   if race.Enabled {
      race.Enable()
   }
}

惰性回收

sync.Pool 的垃圾回收发生在运行时 GC 开始之前。

var poolcleanup func()
// 利用编译器标志将 sync 包中的清理注册到运行时
//go:linkname sync_runtime_registerPoolCleanup sync.runtime_registerPoolCleanup
func sync_runtime_registerPoolCleanup(f func()) {
	poolcleanup = f
}

// 实现缓存清理
func clearpools() {
	// clear sync.Pools
	if poolcleanup != nil {
		poolcleanup()
	}
    (...)
}

清理函数

victim的使用出现在getslow函数中,当从其他Pshared中无法窃取到对象时,会尝试从上一次GC周期时放置的缓存中获取对象。

这就涉及到了惰性回收,当GC触发前poolCleanup会将运行时中所有sync.Pool对象中的poolLocal移动到其对应victim字段,victim会保存一个GC周期后被清除。

func poolCleanup() {

   // 清空上一GC周期的victim缓存
   for _, p := range oldPools {
      p.victim = nil
      p.victimSize = 0
   }

   // 将当前运行时中所有Pool(不同的Pool对象)中的local移动到其victim中
   for _, p := range allPools {
      p.victim = p.local
      p.victimSize = p.localSize
      p.local = nil
      p.localSize = 0
   }

   // 互换oldPool和allPools,并将allPools置nil
   oldPools, allPools = allPools, nil
}

备注

才疏学浅,若有疑惑之处,很可能是笔者出错了。还望不吝赐教。

参考资料

[1] false sharing

2 欧老师:Go source study: sync.Pool

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值