sync.Pool底层源码详解

发布时间 2023-12-29 13:00:57作者: 李若盛开

Golang中sync.Pool用来提高对象复用几率,减少gc的压力,减少内存分配,它是线程安全的,常用来存储并复用临时对象。

任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩。

可伸缩的,其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器。

sync.Pool中的值只在两次GC中间的时段有效。

原理

为了减小并发中锁的竞争,sync.pool为每个P(对象cpu线程)分配一个子池子poolLocal,每个poolLocal有private对象和shared共享列表对象,private私有对象只有对应的P可访问,无需加锁, shared共享列表对象可被其它P共享,需要加锁。

sync.pool结构体

type Pool struct {
    noCopy noCopy   //该对象不能被copy使用
 
    local     unsafe.Pointer // [p]poolLocal,固定长度
    localSize uintptr        //本地缓冲池poolLocal的数量
 
    New func() interface{}  //用户自定义的用于生成对象的方法【当池中没有可用对象时,会调用 New 函数构造构造一个对象】
}

sync.pool的结构组成如上图所示,会有两个疑问:

1)、实例化 Sync.Pool的时候,为什么实例化了一个LocalPool数组,怎么确定我的数据应该存储在LocalPool数组的哪个单元?

  这里的LocalPool是根据不同的pid来区分的,保证private数据的线程安全,程序运行的时候可以获取到pid,然后使用pid作为LocalPool的索引,找到对应的地址即可
2)、PoolLocalInternal 里面的成员有private和shared,为什么要做这两种区分?

  private 是 P 专属的, shared是可以被其他的P获取到的(类似于GMP模型对G的抢占)

Get函数

作用:从Pool中获取一个对象,如果获取不到并且New函数不为空,则通过New创建一个对象并返回;如果 New 未设置,则返回 nil。

源码:

Pool 会为每个 P 维护一个本地池,P 的本地池分为 私有池 private 和共享池 shared。私有池中的元素只能本地 P 使用,共享池中的元素可能会被其他 P 偷走,所以使用私有池 private 时不用加锁,而使用共享池 shared 时需加锁。
Get 会优先查找本地 private,再查找本地 shared,最后查找其他 P 的 shared,如果以上全部没有可用元素,最后会调用 New 函数获取新元素。

func (p *Pool) Get() interface{} {
    if race.Enabled {
        race.Disable()
    }
    //获取当前线程的poolLocal
    l := p.pin()
    //如果private对象不为空则直接返回,并将其置为nil
    x := l.private
    l.private = nil
    runtime_procUnpin()
    if x == nil {
        //private不存在则加锁从shared列表中拿
        l.Lock()
        last := len(l.shared) - 1
        if last >= 0 {
            x = l.shared[last]
            l.shared = l.shared[:last]
        }
        l.Unlock()
        //如果shared对象列表依然没有的话,则从其它P的poolLocal获取
        if x == nil {
            x = p.getSlow()
        }
    }
    if race.Enabled {
        race.Enable()
        if x != nil {
            race.Acquire(poolRaceAddr(x))
        }
    }
    // 如果存在New func回调函数,则执行
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}
View Code

Put()函数

作用:将对象x放入到Pool中,以便复用该对象。

操作逻辑:

1)获取当前执行的Pid
2)根据Pid,找到对应的PoolLocal(数组),接着使用里面PoolLocalInternal(结构体)
2)优先存入 PoolLocalInternal 的 private属性,然后存入PoolLocalInternal 的 shared (slice)里面

源码:

Put 优先把元素放在 private 池中;如果 private 不为空,则放在 shared 池中。有趣的是,在入池之前,该元素有 1/4 可能被丢掉。

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()
    }
    //如果当前poolLocal的private对象为nil,则直接赋值
    l := p.pin()
    if l.private == nil {
        l.private = x
        x = nil
    }
    runtime_procUnpin()
    //否则加到当前poolLocal的shared列表中
    if x != nil {
        l.Lock()
        l.shared = append(l.shared, x)
        l.Unlock()
    }
    if race.Enabled {
        race.Enable()
    }
}
View Code

poolCleanup回收对象

作用:用来清理Pool,但官方的实现略显粗暴。

这个函数会在GC之前调用,这也就解释了官方的下面一句话:

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.
//存储在池中的任何项目都可以随时自动删除,无需通知。如果发生这种情况时 Pool 持有唯一的引用,则项目可能会被取消分配。

如果一个数据仅仅在Pool中有引用,那么就需要担心这个数据被GC清理掉。

源码:

当垃圾回收(STW)将要开始时, poolCleanup 会被调用。

该函数内不能分配内存且不能调用任何运行时函数。原因:

1)防止错误,保留整个 Pool;
2)如果 GC 发生时,某个 goroutine 正在访问 l.shared,整个 Pool 将会保留,下次执行时将会有双倍内存。

func init() {
    runtime_registerPoolCleanup(poolCleanup)
}
 
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
    lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
    return (*poolLocal)(lp)
}
 
// Implemented in runtime.
func runtime_registerPoolCleanup(cleanup func())
func runtime_procPin() int
func runtime_procUnpin()
 
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.
    // Defensively zero out everything, 2 reasons:
    // 1. To prevent false retention of whole Pools.
    // 2. If GC happens while a goroutine works with l.shared in Put/Get,
    //    it will retain whole Pool. So next cycle memory consumption would be doubled.
    for i, p := range allPools {
        allPools[i] = nil
        for i := 0; i < int(p.localSize); i++ {
            l := indexLocal(p.local, i)
            l.private = nil
            for j := range l.shared {
                l.shared[j] = nil
            }
            l.shared = nil
        }
        p.local = nil
        p.localSize = 0
    }
    allPools = []*Pool{}
}
View Code

可以看到在init的时候注册了一个PoolCleanup函数,它会清除掉sync.Pool中的所有的缓存的对象,这个注册函数会在每次GC的时候运行,所以sync.Pool中的值只在两次GC中间的时段有效

案例1:gin 中的 Context pool

在 web 应用中,后台在处理用户的每条请求时都会为当前请求创建一个上下文环境 Context,用于存储请求信息及相应信息等。Context 满足长生命周期的特点,且用户请求也是属于并发环境,所以对于线程安全的 Pool 非常适合用来维护 Context 的临时对象池。

Gin 在结构体 Engine 中定义了一个 pool:

type Engine struct {
   // ... 省略了其他字段
   pool             sync.Pool
}

案例2:fmt 中的 printer pool

printer 也符合长生命周期的特点,同时也会可能会在多 goroutine 中使用,所以也适合使用 pool 来维护。

printer 与 它的临时对象池:

// pp 用来维护 printer 的状态
// 它通过 sync.Pool 来重用,避免申请内存
type pp struct {
   //... 字段已省略
}
 
var ppFree = sync.Pool{
   New: func() interface{} { return new(pp) },
}

总结:

  Get方法并不会对获取到的对象值做任何的保证,因为放入本地池中的值有可能会在任何时候被删除,但是不通知调用者;放入共享池中的值有可能被其他的goroutine偷走。

所以对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,因为存入对象池重的值有可能会在垃圾回收时被删除掉,这违反了数据库连接池建立的初衷。

根据上面的说法,Golang的对象池严格意义上来说是一个临时的对象池,适用于储存一些会在goroutine间分享的临时对象。主要作用是减少GC,提高性能。在Golang中最常见的使用场景是fmt包中的输出缓冲区。