「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
时候传入一个用于生成临时对象的函数,如果要不传入的话当Get
在private
与shared
都没获取到临时对象时,会返回nil
而不是新建。
sync.Pool.poolCleanup
的实现
在前言中说到,sync.Pool
会在不定时间的时候对已创建对象进行清除和Gc,这就需要用到sync.Pool.poolCleanup
函数,其会在GC开始时STW阶段被调用,他将sync.Pool
中victim
中的对象移除,然后把 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
时在private
与shared
都没获取到临时对象时会去垃圾堆victim
找,如果找到了,该对象下次Put
回的时候就不会放到垃圾堆victim
里了。
sync.Pool.Get
的实现
接下来看一下最重要的一个接口Get
的实现,其可以返回一个可用的临时对象,如果有New
这个字段如果初始化时未被传入,则当Get
在private
与shared
都没获取到临时对象时,会返回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
}
其实查找方式很简单,主要有以下几个步骤:
- 先从当前P的
poolLocal
里面找看看private
是否有临时对象可以返回,有的话返回,没有的话查一下自己的shared
有没有。 - 如果没有就去其他P的
poolLocal
的shared
里找看看有没有 - 如果没有就去垃圾堆
victim
里去找 - 如果垃圾堆也没有,就看看是否初始化了
New
,初始化了就重新创建一个,否则返回nil
。
sync.Pool.Put
的实现
相对于### sync.Pool.Get
, sync.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
的实现就简单多了大概步骤如下:
- 如果传入对象是
nil
那就不要他,要他也没用。 - 如果当前
private
是nil
就直接把传入对象赋值给他 - 否则,就放入
shared
的头部,供日后使用
总结
至此 sync.Pool
的源码解析就解析完了,可能有些地方有些理解上的错误,请各位谅解并且帮忙指出修改意见,如果这篇文章能帮到你,这是我的荣幸。