Golang 语言临时对象池 - sync.Pool

简介: Golang 语言临时对象池 - sync.Pool

介绍

sync.Pool 是 sync 包提供的一个数据类型,也称为临时对象池,它的值是用来存储一组可以独立访问的临时对象,它通过池化减少申请新对象,提升程序的性能。sync.Pool 类型是 struct 类型,它的值在被首次使用之后,就不可以再被复制了。因为 sync.Pool 中存储的所有对象都可以随时自动删除,所以使用 sync.Pool 类型的值必须满足两个条件,一是该值存在与否,都不会影响程序的功能,二是该值之间可以互相替代。sync.Pool 是 goroutine 并发安全的,可以安全地同时被多个 goroutine 使用;sync.Pool 的目的是缓存已分配但未使用的对象以供以后重用,从而减轻了垃圾收集器的性能影响,因为 Go 的自动垃圾回收机制,会有一个 STW 的时间消耗,并且大量在堆上创建对象,也会增加垃圾回收标记的时间。

sync.Pool 的适当用法是管理一组临时对象,这些临时对象在程序包的并发独立客户端之间静默共享并有可能被重用。sync.Pool 提供了一种分摊许多客户端上的分配开销的方法。

但是,作为短期(short-lived)对象的一部分维护的空闲列表不适用于 sync.Pool,因为在这种情况下,开销无法很好地分摊。

Golang 语言中的标准库 fmt 包使用了 sync.Pool,它会使用一个动态大小的 buffer 池做输出缓存,当大量的 goroutine 并发输出的时候,就会创建比较多的 buffer,并且在不需要的时候回收掉。

02

使用方式

sync.Poll 类型包含两个方法:

  • func (p *Pool) Put(x interface{})
  • func (p *Pool) Get() interface{}

Put()于向临时对象池中存放对象,它接收一个 interface{} 空接口类型的参数;Get()用于从临时对象池中获取对象,它返回一个 interface{} 空接口类型的返回值。

Get() 从临时对象池中选择一个任意对象,将其从临时对象池中删除,然后将其返回给调用方。 Get() 可以选择忽略临时对象池并将其视为空。调用者不应假定传递给 Put() 的值和 Get() 返回的值之间有任何关系。

如果 Get() 返回 nil,而 p.New 不为 nil,则 Get() 返回调用 p.New 的结果。 sync.Pool 类型的 New 字段,字段类型是函数类型 func() interface{},代表创建临时对象的函数,该函数的结果值并不会存入到临时对象池中,而是直接返回给 Get() 方法的调用方。

需要注意的是,sync.Pool 类型的 New 字段的值也需要我们初始化对象时给定,否则,在调用 Get() 方法时,有可能会得到 nil。

我们已经介绍了临时对象什么时候会被创建,现在我们介绍临时对象什么时候会被销毁。我们已经知道 sync.Pool 使用之前需要先初始化,其实在初始化时,还会向 Golang 运行时中注册一个清理函数,用于清理临时对象池中的所有已创建的值,golang 运行时每次在执行垃圾回收之前,先执行该清理函数。

示例代码:

func main () {
 pool := &sync.Pool{
  New: func() interface{} {
   fmt.Println("New 一个新对象")
   return 0
  },
 }
 // 取,临时对象池中没有数据,会调用 New,New 创建一个新对象直接返回,不会存储在临时对象池中
 val := pool.Get().(int)
 fmt.Println(val)
 // 存
 pool.Put(10)
 // 手动调用 GC(),用于验证 GC 之后,临时对象池中的对象会被清空。
 runtime.GC()
 // 取
 val2 := pool.Get().(int)
 fmt.Println(val2)
}

03

实现原理

在 Go1.13 之前,临时对象池的数据结构中有一个本地池列表,在每个本地池中包含三个字段,分别是存储私有临时对象的字段 private、共享临时对象列表的字段 shared 和 sync.Mutex 类型的嵌入字段。

锁竞争会降低程序的并发性能,想要优化程序的并发性能,就是减少或避免锁的使用。在 Go1.13 中,sync.Pool 做了优化,就是避免使用锁,将加锁的队列改成了无锁的队列,并给即将被移除的元素多一次“复活”的机会。

当前 sync.Pool 的数据结构如下:

type Pool struct {
 noCopy noCopy
 local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
 localSize uintptr        // size of the local array
 victim     unsafe.Pointer // local from previous cycle
 victimSize uintptr        // size of victims array
 // New optionally specifies a function to generate
 // a value when Get would otherwise return nil.
 // It may not be changed concurrently with calls to Get.
 New func() interface{}
}

其中 local 和 victim 主要用于存储空闲元素,每次 GC 时,Pool 会先把 victim 字段的数据移除,然后把 local 字段的数据给 victim,这样 local 等于被清空了,而 local 的数据在 victim 中就有机会再次被 Get() 取走,如果没有 Get() 取走数据,victim 的数据就会被 GC 掉。

阅读下面这段代码,它是 GC 时 sync.Pool 的处理逻辑。

func poolCleanup() {
 // This function is called with the world stopped, at the beginning of a garbage collection.
 // It must not allocate and probably should not call any runtime functions.
 // Because the world is stopped, no pool user can be in a
 // pinned section (in effect, this has all Ps pinned).
 // Drop victim caches from all pools.
 for _, p := range oldPools {
  p.victim = nil
  p.victimSize = 0
 }
 // Move primary cache to victim cache.
 for _, p := range allPools {
  p.victim = p.local
  p.victimSize = p.localSize
  p.local = nil
  p.localSize = 0
 }
 // The pools with non-empty primary caches now have non-empty
 // victim caches and no pools have primary caches.
 oldPools, allPools = allPools, nil
}

本地池列表中本地池的数量和 golang 调度器中 processor 的数量相等,也就是说每个本地池对应一个 P,我们在介绍 GMP 的文章中讲过,一个 goroutine 想要运行,必须先和某个 P 关联。所以临时对象池的 Put()Get() 方法被调用时,会去操作哪个本地池,就取决于调用代码运行的 goroutine 对应的 P,这就是为什么每个本地池对应一个 P。

// Local per-P Pool appendix.
type poolLocalInternal struct {
 private interface{} // Can be used only by the respective P.
 shared  poolChain   // Local P can pushHead/popHead; any P can popTail.
}
type poolLocal struct {
 poolLocalInternal
 // Prevents false sharing on widespread platforms with
 // 128 mod (cache line size) = 0 .
 pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

阅读上面这段代码,poolLocalInternal 结构体中包含两个字段 private 和 shared,private 代表一个缓存元素,只能被当前 P 的 goroutine 存取,因为一个 P 同时只能执行一个 goroutine,所以不会有并发问题。shared 可以被任意 P 访问,但是只能本地 P 可以 pushHead/popHead,其他 P 只能 popTail,它是使用一个无锁队列实现的。

存取数据:

Put() 方法会优先将新创建的临时对象存储在本地的 private 字段,如果 private 字段已经存储了某个值,它才会去访问 shared 字段,把新的临时对象追加到共享临时对象列表的末尾。

Get() 方法会优先访问 private 字段获取数据,因为无锁,获取元素的速度快,如果 private 字段为空时,就会尝试访问 local 的 shared 字段,如果 local 的 shared 字段也是空的,它会调用 getSlow() 方法,遍历每一个 local 的 shared 字段,只要发现某个 local 的 shared 字段有值,就会获取该 shared 共享临时对象列表的最后一个值并返回。如果遍历所有 local 都没有找到值,就会尝试访问 victim,先从 victim 的 private 字段中查找,如果没有找到,再从 victim 的 shared 字段查找,最后,如果都没有获取到,就会调用初始化时的 New 字段给定的创建临时对象的函数创建一个新对象并返回,如果 New 字段的值为 nil,Get() 方法就直接返回 nil。

getSlow() 方法的处理逻辑:

func (p *Pool) getSlow(pid int) interface{} {
 // See the comment in pin regarding ordering of the loads.
 size := runtime_LoadAcquintptr(&p.localSize) // load-acquire
 locals := p.local                            // load-consume
 // Try to steal one element from other procs.
 for i := 0; i < int(size); i++ {
  l := indexLocal(locals, (pid+i+1)%int(size))
  if x, _ := l.shared.popTail(); x != nil {
   return x
  }
 }
 // Try the victim cache. We do this after attempting to steal
 // from all primary caches because we want objects in the
 // victim cache to age out if at all possible.
 size = atomic.LoadUintptr(&p.victimSize)
 if uintptr(pid) >= size {
  return nil
 }
 locals = p.victim
 l := indexLocal(locals, pid)
 if x := l.private; x != nil {
  l.private = nil
  return x
 }
 for i := 0; i < int(size); i++ {
  l := indexLocal(locals, (pid+i)%int(size))
  if x, _ := l.shared.popTail(); x != nil {
   return x
  }
 }
 // Mark the victim cache as empty for future gets don't bother
 // with it.
 atomic.StoreUintptr(&p.victimSize, 0)
 return nil
}

04

总结

本文我们主要介绍了 sync.Pool 数据类型,包括它的使用方式和实现原理,它的优势就是可以复用对象,降低对象的新建和 GC 的开销。我们需要再次强调的是,sync.Pool 的生命周期受 GC 的影响,不适合用来做需要自己管理生命周期的池化,比如连接池。

推荐阅读:

参考资料:

https://golang.org/pkg/sync/#Pool

https://golang.org/src/sync/pool.go?s=1633:2101#L34

目录
相关文章
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
116 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
67 4
Golang语言文件操作快速入门篇
|
2月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
104 3
Golang语言之gRPC程序设计示例
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
88 4
|
2月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
53 4
Golang语言goroutine协程篇
|
2月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
55 3
Golang语言之Prometheus的日志模块使用案例
|
1月前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
33 0
|
2月前
|
JSON Go 数据格式
Golang语言结构体链式编程与JSON序列化
这篇文章是关于Go语言中结构体链式编程与JSON序列化的教程,详细介绍了JSON格式的基本概念、结构体的序列化与反序列化、结构体标签的使用以及如何实现链式编程。
39 4
|
2月前
|
Go
Golang语言结构体(struct)面向对象编程进阶篇(封装,继承和多态)
这篇文章是关于Go语言中结构体(struct)面向对象编程进阶篇的教程,涵盖了Go语言如何实现封装、继承和多态,以及结构体内存布局的相关概念和案例。
155 4
|
2月前
|
Go
Golang语言基础之接口(interface)及类型断言
这篇文章是关于Go语言中接口(interface)及类型断言的详细教程,涵盖了接口的概念、定义、实现、使用注意事项以及类型断言的多种场景和方法。
38 4