sync.Pool「一」|Go主题月

482 阅读3分钟

周末在看 go-zero 中的 Pool 想到 sync.Pool ,看两者实现的区别和使用范围。本篇主要分析 sync.Pool 中设计和 free-lock 的思考方向。

前🧂

不管是连接池,协程池,等等这些,设计的初衷都是解决创建对象(资源)的耗时,通过预创建来减少业务耗时提升性能;当然池的大小也是一定,这个一定程度可以防止资源无限膨胀,当然了膨胀了也需要回收,这个有引起 GC 以及 STW 耗时加长。

设计目的说完了。正式聊聊 sync.Pool ,说说几个问题:Go 1.13 之前的问题

  1. GC 都会回收对象;回收以后,Get 命中率降低,重复又创建一个。而且 GC 引起 STW 会造成性能抖动。
  2. 使用 Mutex ,这个很明显。如果多个 goroutine 获取,竞争+锁必定是会造成性能下降的。

从优化角度:

  1. GC 延迟回收,同时根据在 Get 的时候可以从要 GC 的容器中再次回收对象;
  2. 锁这个问题:
    • 实在是需要锁,那就把锁的粒度降低,也就减少竞争的范围;
    • 直接减少竞争:提前将资源分配好,就没有竞争;

所以对 sync.Pool 的优化就是:对象尽量复用,避免重复创建以及销毁。

当然了,具体怎么做,其实有很多的方式。这个就要到具体的代码逻辑看了,那就开始。

Pool struct

type Pool struct {
    // 保证一个对象在第一次使用后不会发生复制
    noCopy noCopy
    // 这两个是一对
    local     unsafe.Pointer // 每个 P 的本地队列,实际类型为 [P]poolLocal
    localSize uintptr        // [P]poolLocal的大小

    victim     unsafe.Pointer // 可以理解为一次GC之后的垃圾桶,翻垃圾桶看能不能回收点什么回来
    victimSize uintptr        // victim size

    New func() interface{}		// 当没有可对应获取的对象,调用这个new()
}

提一嘴,记录一下这个 noCopy 这个检查机制:

type noCopy struct{}

// go vet -copylocks 静态分析
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}
type poolLocal struct {
    poolLocalInternal

    // 防止伪共享
    // 128 mod (cache line size) = 0 .
    pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

type poolLocalInternal struct {
    private interface{} // 本地的P,本地P是对Pool加锁的,直接操作Pool就行
    shared  poolChain   // 公共缓存区
}

type poolChain struct {
    // 只有生产者会 push to,不用加锁
    head *poolChainElt

    // 只能由消费者操作,读写需要原子控制
    tail *poolChainElt
}

type poolChainElt struct {
    poolDequeue 
    next, prev *poolChainElt
}
type poolDequeue struct {
    // 32位head,32位tail指针
    headTail uint64
    // ring buffer
    vals []eface
}

Get

func (p *Pool) Get() interface{} {
    // 忽略一些 race 检查......
    l, pid := p.pin()
    x := l.private
    l.private = nil
    if x == nil {
        x, _ = l.shared.popHead()
        if x == nil {
                x = p.getSlow(pid)
        }
    }
    runtime_procUnpin()
    // ......
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}

流程如下:

  1. 将当前运行的 GP 绑定,并返回当前 P 对应的 poolLocal, pid【没有的话看后面 pinSlow 创建】
  2. poolLocalprivate,赋值给 x ,然后 private 置为 nil
  3. 判断 x == nil ,若为空,从 local.shared 头部pop一个,赋值给 x
  4. 还为空,那就从别的 Pshared 窃取一个对象【getSlow顾名思义很慢很慢,有一个大锁】
  5. 没有其他地方可以获取对象了,runtime_procUnpin 解除绑定
  6. 最后还是没找到,那就 New() 一个

流程说完了,挑几个在看的过程中感觉有意思的点:

pin

pin()
	|- runtime_procPin()	// 获取pid
	|- pid < localsize -> return local[pid]
	|- pinSlow()	// 一看就很慢

这个时候说明 Pool 还没有创建 poolLocal,那就要根据当前 P 个数,make([]poolLocal, psize)

为什么说很慢呢?

func (p *Pool) pinSlow() (*poolLocal, int) {
    // 直接锁住全部的pool
    allPoolsMu.Lock()
    defer allPoolsMu.Unlock()
    // 接触绑定,可能已经有别的P创建了;
    // 如果这个时候有local那就返回 local[pid]
    // ...
    // 创建
    size := runtime.GOMAXPROCS(0)
    local := make([]poolLocal, size)
    atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
    atomic.StoreUintptr(&p.localSize, uintptr(size))         // store-release
    return &local[pid], pid
}

allPoolsMu 所有的对象池都会在这个数组里,然后这个锁会锁住这个全局的Pool数组。


未完待续。。。