周末在看 go-zero 中的 Pool 想到 sync.Pool ,看两者实现的区别和使用范围。本篇主要分析 sync.Pool 中设计和 free-lock 的思考方向。
前🧂
不管是连接池,协程池,等等这些,设计的初衷都是解决创建对象(资源)的耗时,通过预创建来减少业务耗时提升性能;当然池的大小也是一定,这个一定程度可以防止资源无限膨胀,当然了膨胀了也需要回收,这个有引起 GC 以及 STW 耗时加长。
设计目的说完了。正式聊聊 sync.Pool ,说说几个问题:Go 1.13 之前的问题
GC都会回收对象;回收以后,Get命中率降低,重复又创建一个。而且GC引起STW会造成性能抖动。- 使用
Mutex,这个很明显。如果多个goroutine获取,竞争+锁必定是会造成性能下降的。
从优化角度:
GC延迟回收,同时根据在Get的时候可以从要GC的容器中再次回收对象;- 锁这个问题:
- 实在是需要锁,那就把锁的粒度降低,也就减少竞争的范围;
- 直接减少竞争:提前将资源分配好,就没有竞争;
所以对 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
}
流程如下:
- 将当前运行的
G和P绑定,并返回当前P对应的poolLocal, pid【没有的话看后面pinSlow创建】 - 取
poolLocal的private,赋值给x,然后private置为nil - 判断
x == nil,若为空,从local.shared头部pop一个,赋值给x - 还为空,那就从别的
P的shared窃取一个对象【getSlow顾名思义很慢很慢,有一个大锁】 - 没有其他地方可以获取对象了,
runtime_procUnpin解除绑定 - 最后还是没找到,那就
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数组。
未完待续。。。