当前位置: 首页 > news >正文

Go|sync.Pool|临时对象池,实现临时对象的复用,降低GC压力

基本介绍

  • Pool(对象池)是一组临时对象的集合,这些对象可单独存储和检索。
  • 存储在 Pool 中的任何项都可能在未通知的情况下被自动移除。若发生此情况时,Pool 持有该对象的唯一引用,则该对象可能会被释放(内存回收)。
  • Pool 支持多个 goroutine(Go 协程)同时安全使用。
  • Pool 的用途是缓存已分配但未使用的对象,以便后续复用,从而减轻垃圾回收器(GC)的压力。也就是说,它能轻松构建高效、线程安全的空闲列表(free list)。但并非所有空闲列表场景都适合使用 Pool。
  • Pool 的适用场景是:管理一组临时对象,这些对象由某个包的多个并发独立客户端隐式共享,且可能被客户端复用。
  • Pool 提供了一种方式,可在多个客户端之间分摊对象分配的开销。
  • Pool 的良好使用示例:在 fmt 包中,Pool 用于维护动态大小的临时输出缓冲区存储。该存储会在负载下(多个 goroutine活跃打印时)自动扩容,在闲置时自动缩容。
  • 反之,若空闲列表是短期对象的一部分,则不适合使用 Pool——因为在此场景下,Pool 的开销无法有效分摊。此类对象更适合自行实现空闲列表。
  • Pool 在首次使用后,不得进行拷贝操作。
  • 按照 Go 内存模型的术语:调用 Put(x)(存入对象 x)的操作“先行发生于”(synchronizes before) 调用 Get() 并返回同一对象 x 的操作。
  • 类似地,调用 New()(创建对象)并返回 x 的操作“先行发生于”调用 Get() 并返回同一对象 x 的操作。

适用场景

  • sync.Pool中的对象是临时对象,随时可能被移除,所以不要存储一些带有状态的对象,比如数据库连接等、也不要存储一些需要计数的对象
  • 不适合存储短期对象,因为无法在多个客户端之间分摊压力;适合存储一些长期对象,开销可以被分摊

基本用法

sync.Pool提供了三种方法

方法说明
field New func() anysync.Pool的构造函数,用于指定sync.Pool中缓存的数据类型,当调用Get方法时从对象池中获取对象时,若对象池中没有相应的对象,则用New方法创建一个新的对象
func (p *sync.Pool) Get() any从对象池中获取一个对象
func (p *sync.Pool) Put(x any)往对象池中放入对象,下次Get时复用
type Student struct{Name stringAge int}func main() {var pool sync.Poolpool.New = func() any {return &Student {}}st:=pool.Get().(*Student)st.Name="ccc"st.Age=22fmt.Println(st)fmt.Printf("addr is %p\n",st)// st.Name=""// st.Age=0pool.Put(st)st1:=pool.Get()fmt.Println(st1)fmt.Printf("addr is %p\n",st1)}// 运行结果
&{ccc 22}
addr is 0xc0000a6000
&{ccc 22}
addr is 0xc0000a6000// 隔一段时间再次运行
&{ccc 22}
addr is 0xc000010018
&{ccc 22}
addr is 0xc000010018

结论

  • 隔一段时间打印对象的地址发现地址变了,说明对象会随时被清除然后调用p.New重建,所以不建议存储一些短期使用的对象
  • Get之后将用完的对象通过Put放回对象池,对象才可以复用,如果不Put回去,会重新创建对象
  • 第一次获取对象st对其做了一些修改,调用put重新放回对象池,再次获取对象st1会发现st1的值也被修改了,所以put之前需要先将对象还原

源码解读

Pool结构

type Pool struct {noCopy noCopy  // 用于防止 Pool 结构体被意外拷贝的标记字段(通过编译期检查实现)local      unsafe.Pointer // 每个 P(逻辑处理器)专属的固定大小本地对象池,实际类型为 [P]poolLocallocalSize  uintptr        // local 数组的长度(即 P 的数量,对应 Go 运行时的逻辑处理器个数)victim     unsafe.Pointer // 上一个清理周期的本地对象池(用于渐进式清理,减少 GC 压力)victimSize uintptr        // victim 数组的长度// New 可选地指定一个函数,用于在 Get 方法否则会返回 nil 时生成一个新值。// 调用 Get 方法期间,不得并发修改此函数。New func() any
}// 每个P(逻辑处理器)的本地池附加结构。
type poolLocalInternal struct {private any       // 只能由相应的P使用。shared  poolChain // 本地P可以执行pushHead/popHead操作;任何P都可以执行popTail操作。
}type poolLocal struct {poolLocalInternal// 在缓存行大小满足128 mod(缓存行大小)= 0的主流平台上,防止伪共享。pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}// `src/sync/poolqueue.go`
// poolChain是poolDequeue的动态大小版本。
//
// 它被实现为poolDequeue的双向链表队列,其中每个队列的大小都是前一个的两倍。
// 一旦一个队列填满,就会分配一个新的队列,并且只向最新的队列中推送元素。
// 从链表的另一端进行弹出操作,一旦一个队列被耗尽,它就会从链表中移除。
type poolChain struct {// head是要推送元素的poolDequeue。仅由生产者访问,因此不需要同步。head *poolChainElt// tail是要从尾部弹出元素的poolDequeue。由消费者访问,因此读写必须是原子操作。tail *poolChainElt
}

Get方法

  • 从对象池中取出任意一个对象,并将该对象从对象池中移除,所以要想复用对象一定要用完之后put
// Get 从对象池(Pool)中选择任意一个项,将其从对象池中移除,并返回给调用者。 
// Get 可能会选择忽略对象池,将其视为空池(即不从中获取对象)。 
// 调用者不应假设传入 Put 方法的值与 Get 方法返回的值之间存在任何关联。 
// 若 Get 方法在正常情况下会返回 nil,且 p.New 不为 nil(即已设置对象生成函数),则 Get 会返回调用 p.New() 后的结果(即通过生成函数创建的新对象)。
func (p *Pool) Get() any {if race.Enabled {race.Disable()}// pin 会将当前 goroutine(Go 协程)绑定到 P(逻辑处理器)上,// 禁用抢占机制,并返回该 P 对应的 poolLocal(本地对象池)以及该 P 的 ID。// 调用者在使用完该对象池后,必须调用 runtime_procUnpin () 来解除绑定。l, pid := p.pin()x := l.private// 从l.private中取出对象将该字段置为空,方便新对象的放入l.private = nilif x == nil {// 尝试弹出本地分片(shard)的头部元素。// 为了实现复用的时间局部性(temporal locality),// 优先选择头部元素而非尾部元素。x, _ = l.shared.popHead()// 如果弹出的首部共享对象为nil,if x == nil {// x = p.getSlow(pid)}}// 将协程和本地P解除绑定runtime_procUnpin()if race.Enabled {race.Enable()if x != nil {race.Acquire(poolRaceAddr(x))}}// 对象池中没有可用的对象,调用New方法进行新建对象if x == nil && p.New != nil {x = p.New()}return x}// getSlow 是 Pool 的慢速路径获取方法,当本地 P 的对象池无可用对象时调用,参数 pid 为当前 P(逻辑处理器)的 ID
func (p *Pool) getSlow(pid int) any {// 参考 pin 方法中的注释,了解此处加载操作的内存顺序规则size := runtime_LoadAcquintptr(&p.localSize) // 以“加载-获取”(load-acquire)内存顺序读取 localSize(本地对象池数组长度)locals := p.local                            // 以“加载-消费”(load-consume)内存顺序读取 local(指向本地对象池数组的指针)// 尝试从其他 P 的本地对象池中“窃取”一个元素for i := 0; i < int(size); i++ {// 计算目标 P 的索引:从当前 P 的下一个位置开始,循环遍历所有 P 的本地对象池l := indexLocal(locals, (pid+i+1)%int(size))// 从目标 P 的共享队列尾部弹出元素,若成功获取(x != nil)则直接返回if x, _ := l.shared.popTail(); x != nil {return x}}// 尝试从 victim 缓存(上一清理周期的对象池)中获取元素。// 先尝试从所有主缓存(当前周期的 local 池)中窃取,再尝试 victim 缓存,// 是为了尽可能让 victim 缓存中的对象“老化”(若长时间未被复用,则后续会被 GC 回收)size = atomic.LoadUintptr(&p.victimSize) // 原子读取 victim 缓存数组的长度if uintptr(pid) >= size {                // 若当前 P 的 ID 超出 victim 缓存数组的范围,说明无对应缓存,返回 nilreturn nil}locals = p.victim                          // 读取指向 victim 缓存数组的指针l := indexLocal(locals, pid)               // 获取当前 P 对应的 victim 本地缓存if x := l.private; x != nil {              // 先检查 victim 缓存的 private 字段(仅当前 P 可访问)l.private = nil                         // 清空 private 字段(避免重复获取)return x}// 若 private 字段无可用对象,循环遍历所有 P 的 victim 共享队列,尝试窃取元素for i := 0; i < int(size); i++ {l := indexLocal(locals, (pid+i)%int(size))if x, _ := l.shared.popTail(); x != nil {return x}}// 将 victim 缓存的大小标记为 0,后续调用 get 时无需再尝试访问 victim 缓存(已无可用对象)atomic.StoreUintptr(&p.victimSize, 0)return nil // 所有路径均未获取到对象,返回 nil
}
  • 优先从P的本地对象池获取对象,没有的话,从其它P的本地对象池窃取对象
  • 从P的上一周期清理的本地对象池获取对象,从其它P的上一周期清理的本地对象池获取对象
  • 都没有的话,调用p.New方法新建对象

put方法

// Put adds x to the pool.
func (p *Pool) Put(x any) {if x == nil {return}if race.Enabled {if runtime_randn(4) == 0 {// Randomly drop x on floor.return}race.ReleaseMerge(poolRaceAddr(x))race.Disable()}// 将当前协程和本地P绑定,返回本地对象池l, _ := p.pin()// 优先将对象放入到l的private字段// 好像本地P的runnext字段if l.private == nil {l.private = x} else {// 放入本地对象池链表头部l.shared.pushHead(x)}runtime_procUnpin()if race.Enabled {race.Enable()}}
  • 优先将对象放入本地对象池l的private字段,Get取对象的时候也会优先取出private字段中的对象(并将其置为空)
  • 如果private上有对象,将对象推入链表的头部
http://www.dtcms.com/a/481679.html

相关文章:

  • go语言了解
  • 网站页面高度福建住房城乡建设部网站
  • 【Go】--数组和切片
  • 李宏毅机器学习笔记22
  • 重排反应是什么?从分子变化到四大关键特征解析
  • 服务治理与 API 网关:微服务流量管理的艺术
  • 怎样做企业的网站首页网站开发求职简历
  • 程序设计基础第2周上课前预习
  • 谷歌 chrome 浏览器安装crx插件(hackbar为例)
  • 分布式专题——43 ElasticSearch概述
  • Tomcat 启动后只显示 index.jsp,没有进入你的 Servlet 逻辑
  • 分布式之RabbitMQ的使用(3)QueueBuilder
  • 建立自己网站的好处抖音代运营可以相信吗
  • Flink 状态和 CheckPoint 的区别和联系(附源码)
  • QML学习笔记(三十六)QML的ComboBox
  • 媒介宣发的技术革命:Infoseek如何用AI重构企业传播全链路
  • uniapp开发小程序
  • 浦江县建设局网站国家企业信息信用信息公示网址
  • 2025年燃气从业人员考试真题分享
  • SuperMap iServer 数据更新指南
  • C++基础:(十三)list类的模拟实现
  • 【网络编程】从数据链路层帧头到代理服务器:解析路由表、MTU/MSS、ARP、NAT 等网络核心技术
  • 北京网站seowyhseo网站模板但没有后台如何做网站
  • 对接世界职业院校技能大赛标准,唯众打造高质量云计算实训室
  • 利用人工智能、数字孪生、AR/VR 进行军用飞机维护
  • [特殊字符] Maven 编译报错「未与 -source 8 一起设置引导类路径」完美解决方案(以芋道项目为例)
  • 【CV】泊松图像融合
  • 云智融合:人工智能与云计算融合实践指南
  • Maven创建Java项目实战全流程
  • 泉州市住房与城乡建设网站wordpress弹出搜索