go-sync.pool

官方对对象池的定义

// 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之间。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容