Golang 并发原语 Sync Pool

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的数组,这里的容量PGOMAXPROCS的长度,就是GMPP个数
    • 所以local的数组结构应该是[P_index]poolLocal
  • localSizelocal数组长度
  • victim:存放上一轮被GC回收的local
  • victimSizevictim数组长度
  • 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"缓存亲和性
相关推荐
小蒜学长23 分钟前
springboot基于BS的小区家政服务预约平台(代码+数据库+LW)
java·数据库·spring boot·后端
我命由我1234532 分钟前
Git 暂存文件警告信息:warning: LF will be replaced by CRLF in XXX.java.
java·linux·笔记·git·后端·学习·java-ee
简色1 小时前
预约优化方案全链路优化实践
java·spring boot·后端·mysql·spring·rabbitmq
学编程的小鬼2 小时前
SpringBoot日志
java·后端·springboot
用户4099322502122 小时前
PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前?
后端·ai编程·trae
gopyer2 小时前
180课时吃透Go语言游戏后端开发7:Go语言中的函数
开发语言·游戏·golang·go·函数
HWL56792 小时前
输入框内容粘贴时 &nbsp; 字符净化问题
前端·vue.js·后端·node.js
IT_陈寒2 小时前
「JavaScript 性能优化:10个让V8引擎疯狂提速的编码技巧」
前端·人工智能·后端
武子康2 小时前
大数据-116 - Flink Sink 使用指南:类型、容错语义与应用场景 多种输出方式与落地实践
大数据·后端·flink
lbwxxc2 小时前
go 基础
开发语言·后端·golang