「Golang」sync.Pool的源码解析

前言

在平时我们的业务逻辑中,会出现多次,重复的申请在堆上创建的对象用作他用,当并发量不大的时候,可能往往并不会产生一些什么问题,当时一旦当并发量增长的时候就会发现因为重复在堆上创建对象导致了GC的扫描时间与STW(stop-the-world)很长,导致程序性能的降低,因为大量地创建在堆上的对象,也会影响垃圾回收标记的时间,因此来说频繁的在堆上申请对象说对高并发量的程序性能会产生很大的影响。

此时我们可以采用对象池的方式,去针对某些频繁的且大量重复申请的对象预先的创建或者将用完的对象放回对象池中,以便下回使用的时候免去了重新申请内存的问题,这样就有利于减少业务的耗时,还能提高程序的整体性能。
对象池是一种设计模式也是一种性能优化的方式,对象池(对象池模式)的概念如下:

对象池(英语:object pool pattern)是一种设计模式。一个对象池包含一组已经初始化过且可以使用的对象,而可以在有需求时创建和销毁对象。池的用户可以从池子中取得对象,对其进行操作处理,并在不需要时归还给池子而非直接销毁它。这是一种特殊的工厂对象。
若初始化、实例化的代价高,且有需求需要经常实例化,但每次实例化的数量较少的情况下,使用对象池可以获得显著的效能提升。从池子中取得对象的时间是可预测的,但新建一个实例所需的时间是不确定。

因此我们可以通过建立一个对象池的方式去优化频繁在堆上创建的对象,Go中官方标准库中就提供了一个线程安全的对象池—sync.Pool

sync.Pool

首先针对sync.Pool官方给了一个合理的简述:

// A Pool is a set of temporary objects that may be individually saved and retrieved.
sync.Pool是一组临时对象,可以单独保存和检索。
// Any item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.
存储在sync.Pool中的任何项目可随时自动删除,无需通知。如果发生这种情况时sync.Pool持有唯一的引用,则该项目可能会被释放。
// A Pool is safe for use by multiple goroutines simultaneously.
// 一个sync.Pool可以安全地同时供多个Goroutine使用。

总的来说大概就是以下几点:

  • sync.Pool这玩意儿存的是一堆 临时对象
  • 根据上一条的 临时对象 的含义就是sync.Pool里面存储的相关对象
  • 这些 临时对象 随时可能被抛弃掉,这个抛弃不是指的后面说的GC清除,而是直接将这个 临时对象 抛弃不要将其设置为nil
  • 另外,sync.Pool里面的 临时对象 也可随时会被GC清除,但是GC清除的前提是这个 临时对象 没有被任何除sync.Pool之外的东西引用,才会被GC清除。
  • 一个sync.Pool可以安全地同时供多个Goroutine使用。每个sync.Pool都是绑定其对应的GMP模型中的P的(默认读者已经知道了GMP是个啥)。

接下来我来讲解一下sync.Pool的结构体内容和其所提供的三个接口New、Get、Put的相关代码实现。

sync.Pool的结构体实现:

type Pool struct {
	// noCopy 结构体,从而能看出 sync.Pool与Mutex等一样是不能复制的
	noCopy noCopy
	// local 字段 存储的是一个 存储  [P]poolLocal 类型的数组指针,其中P代表runtime.GOMAXPROCS()设置的值
	// 这是一个本地的池,几乎所有临时对象的存取都在这个字段完成
	local     unsafe.Pointer 
	// local存存储  [P]poolLocal的P大小 代表runtime.GOMAXPROCS()设置的值 
	localSize uintptr        

	// 这两个字段与local两个字段的存储东西相同,但是区别是
	// victim存储的是local抛弃下的数组,随时会被gc清除
	// 但是也有可能被捡回去重新使用
	// 可以把这个字段理解为一个随时抛弃随时捡起的垃圾堆
	victim     unsafe.Pointer  	
	victimSize uintptr         
	
	// 这个是唯一开放的字段,用于在初始化的时候传入构建新临时对象的函数
	// 如果这个字段为nil 在调用Get的且没有可用临时对象的时候就不会创建新对象而是返回nil
	New func() interface{}
}

// 这个结构体是一个存储着临时对像和全局对象链的结构体
 type poolLocalInternal struct {
	 // 临时对象存储在这里 这个对象只会被一个P 使用因此无需加锁
	private interface{}  
	// 这是一个无锁队列,有点类似GMP模型里面的全局任务队列(概念类似),当private 就回去里面取
	//hared,可以由任意的 P 访问,但是只有本地的 P 才能 pushHead/popHead,其它 P 可以 popTail,
	shared  poolChain   
}
// 一个P对应一个该结构体
type poolLocal struct {
	poolLocalInternal

	// 做了内存对齐
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

结构体中,只有New这个字段是可以包外访问的,这个字段需要在初始化sync.Pool时候传入一个用于生成临时对象的函数,如果要不传入的话当Getprivateshared都没获取到临时对象时,会返回nil而不是新建。

sync.Pool.poolCleanup的实现

在前言中说到,sync.Pool会在不定时间的时候对已创建对象进行清除和Gc,这就需要用到sync.Pool.poolCleanup函数,其会在GC开始时STW阶段被调用,他将sync.Poolvictim中的对象移除,然后把 local的数据给victim,这样的话,local就会被清空,而 victim就像一垃圾堆,里面的东西可能会被当做垃圾丢弃了,但是里面有用的东西也可能被捡回来重新使用。

func poolCleanup() {
	// oldPools 是一个全局的[]*Pool变量
	// 存的是
	for _, p := range oldPools {
		p.victim = nil
		p.victimSize = 0
	}

	// 移动local 到 victim
	for _, p := range allPools {
		p.victim = p.local
		p.victimSize = p.localSize
		p.local = nil
		p.localSize = 0
	}

	// 此时 所有的 Pool 里面的victim 都是no nil
	// 而local 是nil
	oldPools, allPools = allPools, nil
}

那么什么时候垃圾堆victim中的对象会重新使用呢?就是当Get时在privateshared都没获取到临时对象时会去垃圾堆victim找,如果找到了,该对象下次Put回的时候就不会放到垃圾堆victim里了。

sync.Pool.Get的实现

接下来看一下最重要的一个接口Get的实现,其可以返回一个可用的临时对象,如果有New这个字段如果初始化时未被传入,则当Getprivateshared都没获取到临时对象时,会返回nil而不是新建。

func (p *Pool) Get() interface{} {
	// pin 函数是用于将当前goroutine固定在当前的P上 
	// 为的是防止突然的上下文切换被其他的P执行了
	l, pid := p.pin()
	// 获取当前本地的 临时对象
	x := l.private
	// 临时对象 设为nil
	l.private = nil
	// 如果本地没有
	if x == nil {
		// 就去自己的shared里面找,因为是自己的所以从Head处获取
		// 如果是别人的就从Tail处获取
		x, _ = l.shared.popHead()
		// 还是没有
		if x == nil {
		// 去其他的P的poolLocalInternal里去 “偷”
			x = p.getSlow(pid)
		}
	}
	// 和pin 是相反的
	runtime_procUnpin()
 	 // 如果还是没找到,并且New被设置了就新建一个 否则返回nil
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

// 去别的P的poolLocalInternal“偷”
func (p *Pool) getSlow(pid int) interface{} {
 	// 获取有多少个p
 	size := atomic.LoadUintptr(&p.localSize) 
 	 // 获取最开始的指针
	locals := p.local                         
	// 每个P的poolLocalInternal的share都看看 看看有没有可“偷”的临时对象,有就返回
	for i := 0; i < int(size); i++ {
		l := indexLocal(locals, (pid+i+1)%int(size))
			// 因为是别人的shared所以就从Tail处获取
		if x, _ := l.shared.popTail(); x != nil {
			return x
		}
	}
	
	// 没有就去victim 垃圾堆里面去找,找的方式和 “偷”一样
	 size = atomic.LoadUintptr(&p.victimSize)
	if uintptr(pid) >= size {
		return nil
	}
	locals = p.victim
	l := indexLocal(locals, pid)
	// 先从垃圾堆的 private 找 没有就去垃圾堆的shared去“偷”
	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
		}
	}
	// 如果垃圾堆中都没有,则把这个victim标记为空,以后的查找就可以忽略
	atomic.StoreUintptr(&p.victimSize, 0)

	return nil
}

其实查找方式很简单,主要有以下几个步骤:

  1. 先从当前P的poolLocal里面找看看private是否有临时对象可以返回,有的话返回,没有的话查一下自己的shared有没有。
  2. 如果没有就去其他P的poolLocalshared里找看看有没有
  3. 如果没有就去垃圾堆victim里去找
  4. 如果垃圾堆也没有,就看看是否初始化了New,初始化了就重新创建一个,否则返回nil

sync.Pool.Put的实现

相对于### sync.Pool.Getsync.Pool.Put的实现就简单多了:

 func (p *Pool) Put(x interface{}) {
// 如果返回的x是nil 直接忽略
	if x == nil {
		return
	}
	 // 同样将当前goroutine固定在当前的P上 
	l, _ := p.pin()
	// 如果当前P的private是nil 就放在上面
	if l.private == nil {
		l.private = x
		x = nil
	}
	// 如果不是那就放到当前P的shared队列头上
	if x != nil {
		l.shared.pushHead(x)
	}
		// 和pin 是相反的
	runtime_procUnpin()
}

sync.Pool.Put的实现就简单多了大概步骤如下:

  1. 如果传入对象是 nil那就不要他,要他也没用。
  2. 如果当前privatenil就直接把传入对象赋值给他
  3. 否则,就放入shared的头部,供日后使用

总结

至此 sync.Pool的源码解析就解析完了,可能有些地方有些理解上的错误,请各位谅解并且帮忙指出修改意见,如果这篇文章能帮到你,这是我的荣幸。

我已开通自己的公众号【Echo的技术笔记】
日后的文章发布会主要在公众号上发布
希望各位关注一下
谢谢大家啦
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值