美文网首页
go-sync.pool

go-sync.pool

作者: GGBond_8488 | 来源:发表于2020-03-19 18:43 被阅读0次

    官方对对象池的定义

    // A Pool is a set of temporary objects that may be individually saved andretrieved.
    //
    // 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.
    //
    // A Pool is safe for use by multiple goroutines simultaneously.
    //
    // Pool's purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector. That is, it makes it easy to build efficient, thread-safe free lists. However, it is not suitable for all free lists

    官方对对象池使用场景的描述

    // An appropriate use of a Pool is to manage a group of temporary items

    // silently shared among and potentially reused by concurrent independent clients of a package. Pool provides a way to amortize allocation overhead across many clients.
    //
    // On the other hand, a free list maintained as part of a short-lived object is not a suitable use for a Pool, since the overhead does not amortize well in that scenario. It is more efficient to have such objects implement their own free list.
    //
    // A Pool must not be copied after first use.

    我个人的理解pool就是一个可以回收资源的对象的地方,可以避免对同一个对象重复的创建销毁,从而来节省开销。(但当维护pool本身的开销大于创建销毁对象的开销时,pool就不实用了。)

    pool的结构

    /*
    noCopy          防止copy(一个对象池在首次使用以后就不允许copy了)
    //防止方法就是一旦检测到存在这个nocopy字段就时不允许copy了
    local               本地对象池
    localSize        本地对象池的大小
    New               生成对象的接口方法
    */
    type Pool struct {
        noCopy noCopy
    
        local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
        localSize uintptr        // size of the local array
    
        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 //每个p对应的pool
    
        // Prevents false sharing on widespread platforms with
        // 128 mod (cache line size) = 0 .
           //防止“false sharing/伪共享”
    /*
    缓存系统中是以缓存行为单位存储的。
    缓存行通常是 64 字节,当缓存行加载其中1个字节时候,其他的63个也会被加载出来,
    加锁的话也会加锁整个缓存行,当下图所示x、y变量都在一个缓存行的时候,
    当进行X加锁的时候,正好另一个独立线程要操作Y,这会儿Y就要等X了,此时就不无法并发了。
    */
        pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
    }
    

    pin

    在介绍get/put前,关键的基础函数pin需要先了解一下。 一句话说明用处:确定当前P绑定的localPool对象 (这里的P,是MPG中的P)

    MPG是GO中调度器的模型 实际上包括四个结构
    G:goroutine,每个Goroutine对应一个G结构体,存储运行堆栈,状态,以及任务函数
    P:processer,对于G来说相当于逻辑的处理器,G只有获得了p才能运行,对M来说,p是相关运行环境和上下文,(内存分配状态,任务队列)
    M:Machine:对应着OS内核级线程,代表着真正执行计算的资源,再绑定有效的p后,进入schedule循环。M的数量是不定的,由Go runtime调整。
    S:sched:Go的调度器,维护由M和G的队列以及调度器的一些状态信息。

    // pin pins the current goroutine to P, disables preemption and
    // returns poolLocal pool for the P and the P's id.
    // Caller must call runtime_procUnpin() when done with the pool.
    func (p *Pool) pin() (*poolLocal, int) {
        pid := runtime_procPin()
        // In pinSlow we store to local and then to localSize, here we load in opposite order.
        // Since we've disabled preemption, GC cannot happen in between.
        // Thus here we must observe local at least as large localSize.
        // We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).
        s := atomic.LoadUintptr(&p.localSize) // load-acquire
        l := p.local                          // load-consume
        if uintptr(pid) < s {
            return indexLocal(l, pid), pid
        }
        return p.pinSlow()
    }
    

    1.12之前的GETPUT操作可以总结为下图,在后面的1.13去掉了shared锁,将shared变成双端队列,并引入了受害者队列


    sync.Pool workflow in Go 1.12

    PUT

    优先放入private空间,后面再放入shared空间

    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
        l, _ := p.pin()
    //如果private为nil,则优先进行设置,并标记x
        if l.private == nil {
            l.private = x
            x = nil
        }
    //
        // 如果标记x不为nil,则将x设置到shared队列头中
        if x != nil {
            l.shared.pushHead(x)
        }
        runtime_procUnpin()
        if race.Enabled {
            race.Enable()
        }
    }
    

    GET

    优先从private空间拿,再加锁从shared空间拿,还没有再从其他的PoolLocal的shared空间拿,还没有就直接new一个返回

    func (p *Pool) Get() interface{} {
        if race.Enabled {
            race.Disable()
        }
    // 获取当前的poolLocal
        l, pid := p.pin()
     // 从private中获取
        x := l.private
        l.private = nil
     // 不存在,则继续从shared空间拿
        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()
            if x == nil {
    //如果没有,从其他P的pool中拿
                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
    }
    
    func (p *Pool) getSlow(pid int) interface{} {
        // See the comment in pin regarding ordering of the loads.
    // 获取poolLocal数组的大小
        size := atomic.LoadUintptr(&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
            }
        }
    
        // 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.
        size = atomic.LoadUintptr(&p.victimSize)
        if uintptr(pid) >= size {
            return nil
        }
    //从victim队列里面拿
        locals = p.victim
        l := indexLocal(locals, pid)
        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
            }
        }
    
        // Mark the victim cache as empty for future gets don't bother
        // with it.
        atomic.StoreUintptr(&p.victimSize, 0)
    
        return nil
    }
    

    Go 1.13 版将 shared 用一个双向链表poolChain 作为储存结构,这次改动删除了锁并改善了 shared 的访问。以下是 shared 访问的新流程:

    每个处理器可以在其 shared 队列的头部 push 和 pop,而其他处理器访问 shared 只能从尾部 pop。由于 next/prev 属性,shared 队列的头部可以通过分配一个两倍大的新结构来扩容,该结构将链接到前一个结构。初始结构的默认大小为 8。这意味着第二个结构将是 16,第三个结构 32,依此类推。

    新引进的victim 缓存关于引入 victim 缓存的 commit,所谓受害者缓存 Victim Cache,、容量很小的全相联缓存。当一个数据块被逐出缓存时,并不直接丢弃,而是暂先进入受害者缓存。如果受害者缓存已满,就替换掉其中一项。当进行缓存标签匹配时,在与索引指向标签匹配的同时,并行查看受害者缓存,如果在受害者缓存发现匹配,就将其此数据块与缓存中的不匹配数据块做交换,同时返回给处理器。),新策略非常简单。现在有两组池:活动池和存档池allPoolsoldPools。当 GC 运行时,它会将每个池的引用保存到池中的(victim),然后在清理当前池之前将该组池变成存档池
    (为了保证GC后pool还有对象可用)
    既让对象至少存活两个 GC 区间。

    // 从所有 pool 中删除 victim 缓存
    for _, p := range oldPools {
       p.victim = nil
       p.victimSize = 0
    }
    
    // 把主缓存移到 victim 缓存
    for _, p := range allPools {
       p.victim = p.local
       p.victimSize = p.localSize
       p.local = nil
       p.localSize = 0
    }
    

    // 非空主缓存的池现在具有非空的 victim 缓存,并且池的主缓存被清除
    oldPools, allPools = allPools, nil

    应用程序现在将有一个循环的 GCo 来 创建 / 收集 具有备份的新元素,这要归功于 victim 缓存。在之前的流程图中,将在请求 "shared" pool 的流程之后请求 victim 缓存。

    pool的生命周期

    看一下sync/pool.go文件会给我们展示一个初始化函数,这个函数里面的内容:

    func init() {
       runtime_registerPoolCleanup(poolCleanup)
    }
    
    func gcStart(trigger gcTrigger) {
       [...]
       // clearpools before we start the GC
       clearpools()
    

    当调用垃圾回收时,性能会下降。pools在每次垃圾回收启动时都会被清理。这个文档其实已经有警告我们.
    所以pool的生命周期其实是在两次GC之间。

    相关文章

      网友评论

          本文标题:go-sync.pool

          本文链接:https://www.haomeiwen.com/subject/txxlyhtx.html