Pool 对象池
- 主要适用于大量对象资源被反复构造和回收的场景
- 可以进行一个资源的回收利用、减轻GC压力提高性能
内部数据结构
go
type Pool struct {
noCopy noCopy
local unsafe.Pointer
localSize uintptr
victim unsafe.Pointer
victimSize uintptr
New func() any
}
nocopy
:防止赋值的标识local
:类型为[P]poolLocal
的数组,这里的容量P
为GOMAXPROCS
的长度,就是GMP
的P
个数- 所以local的数组结构应该是
[P_index]poolLocal
- 所以local的数组结构应该是
localSize
:local
数组长度victim
:存放上一轮被GC
回收的local
victimSize
:victim
数组长度New
:创建对象的函数
local
数组的数据结构
go
type poolLocal struct {
poolLocalInternal
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
type poolLocalInternal struct {
private any
shared poolChain
}
poolLocal
:为 Pool 中对应于某个P的缓存数据poolLocalInternal.private
:私有的只能存储一个对象,可无锁化进行访问poolLocalInternal.shared
:本地P的的队列,如果是自己去取队列中的对象,可以保证无锁化访问,但是如果是窃取其他P本地对队列的对象就需要进行加锁操作
核心方法
go
var (
allPoolsMu Mutex
allPools []*Pool
oldPools []*Pool
)
func (p *Pool) pin() (*poolLocal, int) {
if p == nil {
panic("nil Pool")
}
// 禁止当前的goroutine被抢占
// 取出当前P的index,并将当前gorouine与P进行绑定
// 这个时候的goroutine处于一个不可被抢占的状态
pid := runtime_procPin()
// 原子方法取localSzie数组长度
s := runtime_LoadAcquintptr(&p.localSize)
// 取数组
l := p.local
// 如果是第一次执行这个pin方法是不会走入判断中的,直接进入pinSlow方法中
// 这里是取到P的index 和 数组的长度比较
// 如果不是第一次执行,那么数组的长度为P的数量
// 所以在不出意外的情况下,不是第一次就会直接调用indexLocal方法
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
// 如果是第一次调用pin方法那么就会直接先调用pinSlow方法进行local初始化等操作
return p.pinSlow()
}
func (p *Pool) pinSlow() (*poolLocal, int) {
// 这里在调用pinSlow方法之前pin方法调用了runtime_procPin方法
// 这里要进行一个接触绑定Unpin()
runtime_procUnpin()
// 上锁(全剧锁)
allPoolsMu.Lock()
// 解锁
defer allPoolsMu.Unlock()
// 重新进行一个绑定,将当前goroutine这是为不可抢占,获取P的index
pid := runtime_procPin()
// 获取local数组长度和lcoal数组
// 这里其实进行了一个double check
// 毕竟已经加锁了,再次进行一个确认
s := p.localSize
l := p.local
// 如果已经出实话过直接返回
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
// 判断当前数组是否为nil
if p.local == nil {
// 将当前的pool添加到全局的pool中
// GC扫描的时候会将这个全局allPool清空
allPools = append(allPools, p)
}
// 计算、初始化数组长度
size := runtime.GOMAXPROCS(0)
local := make([]poolLocal, size)
// 这里就是适用原子操作发布新数组的指针和大小
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0]))
runtime_StoreReluintptr(&p.localSize, uintptr(size))
return &local[pid], pid
}
- 调用pin方法之后会先将当前goroutine与P进行绑定,并且让当前的goroutine进入一个不可以被抢占的状态。
- 然后进行获取当前pool中local数组和数组长度,然后进行判断
- 如果输入长度大于当前P的index说明存在直接返回
- 但是如果是第一次调用pin方法的情况下,就要进行一个初始化操作
- 因为之前的绑定操作,所以要先进行一个解绑。加锁,进行一个再次确认
- double check还是没有初始化的情况下,就进行开始初始化local数组和lcoalSize
- 然后进行一个返回
Put
go
// Put adds x to the pool.
func (p *Pool) Put(x any) {
// 判断是不是nil
if x == nil {
return
}
// 将当前P和goroutine进行一个绑定,并且获取P的缓存数据
l, _ := p.pin()
// 判断当前P缓存中的私有元素是否为空
if l.private == nil {
// 将当前元素x设置为当前P的私有元素
l.private = x
} else {
// 将x添加到当前P队列的头部
l.shared.pushHead(x)
}
// 解除绑定
runtime_procUnpin()
}
Get
go
func (p *Pool) Get() any {
// 将当前P与当前的goroutine进行一个绑定防止被抢占,并获取到P的缓存数据
l, pid := p.pin()
// 获取到当前P的私有元素
x := l.private
// 获取到私元素后将当前P的私有元素设置为nil
l.private = nil
// 如果当前P的私有元素为nil
if x == nil {
// 尝试从当前P的头部队列取获取元素
x, _ = l.shared.popHead()
// 如果还是获取不到直接调用getSlow方法
if x == nil {
x = p.getSlow(pid)
}
}
// 解除绑定
runtime_procUnpin()
// 如果这里x还是nil的情况就要进行调用New方法初始化构造元素返回
if x == nil && p.New != nil {
x = p.New()
}
return x
}
func (p *Pool) getSlow(pid int) any {
// 原子获取当前pool的local数组大小
size := runtime_LoadAcquintptr(&p.localSize)
// 获取当前pool的数组
locals := p.local
// 这里要进行遍历从其他的P中去找
for i := 0; i < int(size); i++ {
// 获取到其他的P信息
l := indexLocal(locals, (pid+i+1)%int(size))
// 从其他P的队列尾部获取元素 获取到就返回
// 从尾部获取是为了减少竞争,而且不用加锁
// 获取到直接返回
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// 如果还是获取不到 就要取副本中找
size = atomic.LoadUintptr(&p.victimSize)
// 判断当前的P的index是不是大雨等于 副本数组长度
// 如果是就返回nil肯定找不到的
if uintptr(pid) >= size {
return nil
}
// 获取副本数组信息
locals = p.victim
// 依旧是获取副本数组中P的信息
// 找到就直接返回
l := indexLocal(locals, pid)
if x := l.private; x != nil {
l.private = nil
return x
}
// 找不到就只能遍历整个副本数组
// 从其他P的本地队列的尾部获取
// 然后判断 找到就返回
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// 还是获取不到就直接将副本缓存释放掉了
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
poolCleanup
- 在下一轮GC开始的时候执行
poolCleanup
函数
ini
func poolCleanup() {
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
- 这个就很好理解,在被GC之前先将当前
allPools
备份到oldPools
中 - 就是有点缓存的感觉和备份的感觉,双重保障吧
tips
- 绑定P的作用在于:需要P所提供的"竞争隔离域"和"CPU"缓存亲和性