pool 减小GC压力的缓存池

扫描关注获取更多好文
在这里插入图片描述

sync.Pool

回顾总结

看文这篇文章,我们来总结一下精华

  1. sync.Pool是用来缓解频繁的申请内存和缓解因此带来的GC压力的
  2. 在导入pool包时候init会箱GC注册poolCleanup函数,也就是在GC执勤啊运行此函数
  3. P有私有对象池(一个对象位置),公有对象池(多个)
    1. 优先从自己的本地私有local获取,只有一个,同一时刻P只能运行一个goroutine,速度是最快的
    2. 如果私有对象没有,那么获取共享池的从头部弹出一个CompareAndSwapUint64原子操作的置换
    3. 如果公有池也没有,那么去其他P的shard获取一个
    4. 最后都没有,那么就只能自己创建一个
  4. go1.13之后用victim代替mutex锁,GC的时候会直接释放victim,如果victim中对象再次被使用,会返回到存活对象中
  5. noCopy保证是一个空结构,用来防止pool在第一次使用后被复制
  6. poolLocal有pad来防止sharding
  7. 每次GC的将victim设置为空,将原来的local给到victim中

1、false sharding

系统缓存是以行为存储单位的(cache line)为单位存储的,缓存行是2的整数幂个连续字节,一般为32-256个字节,最常见的缓存行大小为64个字节或者128个字节

当系统中多线程修改相互独立变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

伪共享是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素,无声的杀手

我们暂且知道这个,后面会用到

2、数据结构

Go 1.13之后

type Pool struct {
	noCopy noCopy //noCopy 是一个空结构,用来防止 pool 在第一次使用后被复制

	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal,per-P pool, 实际类型为 [P]poolLocal
	localSize uintptr        // size of the local array,local size

	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{}
}

// 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 . 避免缓存 false sharing,使不同的线程操纵不同的缓存行,多核的情况下提升效率。
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

Go1.13 之前

type Pool struct {
    noCopy noCopy // noCopy 是一个空结构,用来防止 pool 在第一次使用后被复制

    local     unsafe.Pointer // per-P pool, 实际类型为 [P]poolLocal
    localSize uintptr        // local 的 size

    // New 在 pool 中没有获取到,调用该方法生成一个变量
    New func() interface{}
}

// 具体存储结构
type poolLocalInternal struct {
    private interface{}   // 只能由自己的 P 使用
    shared  []interface{} // 可以被任何的 P 使用,使用的时候需要加锁
    Mutex                 // 保护 shared 线程安全
}

type poolLocal struct {
    poolLocalInternal

    // 避免缓存 false sharing,使不同的线程操纵不同的缓存行,多核的情况下提升效率。
    pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

var (
    allPoolsMu Mutex
    allPools   []*Pool     // 池列表 
)

以上数据结构我们可以知道

  1. 本地池-poolLocal

    sync.Pool 为每个P(对应CPU)都分配一个本地池。当进行get或者put的时候,会先将goroutine和某个P的子池关联,在对子池操作,后面我们来验证

  2. 本地池分为私有对象和共有对象

    私有对象private interface{}只能自己的P使用,而且同一时刻P运行的肯定只有一个goroutine,可以看到只能存放一个复用对象

    公有对象 shared []interface{}可以被任何的P使用,使用的时候需要加锁

  3. poolLocal中的pad

    poolLocal中有个pad成员,目的是为了防止false sharding,将剩余的cache line 占用,也可以看出golang的线程cache line是128字节

  4. victim老生代回收站,去除mutex锁

    victim有点像java垃圾回收策略的新生代,如果有在被使用的对象,就会放回到local中,如果没有,垃圾回收的时候就会被移除

3、回收逻辑

//此操作在GC的时候出发,直接释放掉victim
func poolCleanup() {
    // 直接丢弃当前victim,因为victim没有使用的,是从local拷贝过来的,没有锁操作
    for _, p := range oldPools {
        p.victim = nil
        p.victimSize = 0
    }

    // 将local复制给victim, 并将原local置为nil
    for _, p := range allPools {
        p.victim = p.local
        p.victimSize = p.localSize
        p.local = nil
        p.localSize = 0
    }

    oldPools, allPools = allPools, nil
}

每次GC的将victim设置为空,将原来的local给到victim中

// Implemented in runtime.
func runtime_registerPoolCleanup(cleanup func())

在runtime中实现,每次gc的时候都会进行清理和复制,其实也就是清理local的信息,间接证明sync.Pool减小GC压力是避免频繁的申请内存和释放内存

4、Get 源码

func (p *Pool) Get() interface{} {
	if race.Enabled {
		race.Disable()
	}
  //将运行的goroutine和P绑定
	l, pid := p.pin()
  //尝试从私有对象获取,并将私有对象nil
	x := l.private
	l.private = nil
  //如果私有对象为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()
    //如果公有池还是没有尝试去其他P的公有池获取一个
		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
}
  1. 优先从自己的本地私有local获取,只有一个,同一时刻P只能运行一个goroutine,速度是最快的
  2. 如果私有对象没有,那么获取共享池的从头部弹出一个CompareAndSwapUint64原子操作的置换
  3. 如果公有池也没有,那么去其他P的shard获取一个
  4. 最后都没有,那么就只能自己创建一个

去其他池获取过程

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.
  //尝试从其他的P中偷取一个
	for i := 0; i < int(size); i++ {
		l := indexLocal(locals, (pid+i+1)%int(size))
		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.
  //如果获取的还是nil,那么就从victim中试试
	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//优先从vimtic的私有池中获取
		return x
	}
	for i := 0; i < int(size); i++ {//从victim的其他P尝试获取
		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.
  //如果没有就标记为nil
	atomic.StoreUintptr(&p.victimSize, 0)

	return nil
}

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()
  //如果本地队列为nil,设置本地队列
	if l.private == nil {
		l.private = x
		x = nil
	}
  //如果本地队列不为nil,加入shard中,从头部加入,记得弹出的时候也会从头部弹出
	if x != nil {
		l.shared.pushHead(x)
	}
	runtime_procUnpin()
	if race.Enabled {
		race.Enable()
	}
}

put 的逻辑是优先设置P的本地队列,因为本地私有队列只有一个位置,如果不为空,那么就加入P的共享池中

6、内存泄漏问题

看极客大佬鸟窝的一个案例

var buffers = sync.Pool{
  New: func() interface{} { 
    return new(bytes.Buffer)
  },
}

func GetBuffer() *bytes.Buffer {
  return buffers.Get().(*bytes.Buffer)
}

func PutBuffer(buf *bytes.Buffer) {
  buf.Reset()
  buffers.Put(buf)
}

案例是很简单,设置buffer,然后返回

这样会有什么问题呢?

没有指定获取和返回的buffer大小,如果put的时候的buffer远远大于初始的buffer大小,且一直增涨的趋势,那么就会造成内存泄漏

最好是指定池子buffer的大小,get的时候根据需要获取对应的池子buffer,put的时候校验buffer的大小,避免内存泄漏

7、内存浪费

还有很多场景是池子的buffer很大,但是我嗯只需要很小的buffer,造成了浪费

我们可以设置多个固定大小的,按需获取

在标准库中net/http/server.go中就存在2k和4k的两个write池子

8、第三方库

 bytebufferpool    https://github.com/valyala/bytebufferpool
 oxtoacart/bpool    https://github.com/oxtoacart/bpool
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

a...Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值