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"缓存亲和性
相关推荐
毅航3 小时前
从原理到实践,讲透 MyBatis 内部池化思想的核心逻辑
后端·面试·mybatis
展信佳_daydayup3 小时前
02 基础篇-OpenHarmony 的编译工具
后端·面试·编译器
Always_Passion3 小时前
二、开发一个简单的MCP Server
后端
用户721522078773 小时前
基于LD_PRELOAD的命令行参数安全混淆技术
后端
笃行3503 小时前
开源大模型实战:GPT-OSS本地部署与全面测评
后端
知其然亦知其所以然4 小时前
SpringAI:Mistral AI 聊天?一文带你跑通!
后端·spring·openai
庚云4 小时前
🔒 前后端 AES 加密解密实战(Vue3 + Node.js)
前端·后端
超级小忍4 小时前
使用 GraalVM Native Image 将 Spring Boot 应用编译为跨平台原生镜像:完整指南
java·spring boot·后端
倔强的石头5 小时前
Mihomo party如何在linux上使用
后端
灵魂猎手5 小时前
11. Mybatis SQL解析源码分析
java·后端·源码