什么是sync.Pool
常使用 sync.Pool 来缓存对象
对于很多需要重复分配、回收内存的地方,sync.Pool
是一个很好的选择。频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺
而 sync.Pool
可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。
当多个 goroutine 都需要创建同⼀个对象的时候,如果 goroutine 数过多,导致对象的创建数⽬剧增,进⽽导致 GC 压⼒增大。形成 “并发⼤-占⽤内存⼤-GC 缓慢-处理并发能⼒降低-并发更⼤”这样的恶性循环
在这个时候,需要有⼀个对象池,每个 goroutine 不再⾃⼰单独创建对象,⽽是从对象池中获取出⼀个对象(如果池中已经有的话)
关键思想:
对象的复用,避免重复创建、销毁
举个简单例子:
package main
import (
"fmt"
"sync"
)
var pool *sync.Pool
type Person struct {
Name string
}
func initPool() {
pool = &sync.Pool {
New: func()interface{} {
fmt.Println("Creating a new Person")
return new(Person)
},
}
}
func main() {
initPool()
p := pool.Get().(*Person)
fmt.Println("首次从 pool 里获取:", p)
p.Name = "first"
fmt.Printf("设置 p.Name = %s\n", p.Name)
pool.Put(p)
fmt.Println("Pool 里已有一个对象:&{first},调用 Get: ", pool.Get().(*Person))
fmt.Println("Pool 没有对象了,调用 Get: ", pool.Get().(*Person))
}
其实在标准库
encoding/json和fmt中
也都用到了 sync.Pool
来提升性能。还有 gin
框架,对 context 取用也到了 sync.Pool
。
接下来我们来看看sync.Pool是如何实现的?
Pool结构体
首先来看 Pool 的结构体:
type Pool struct {
// 防止该结构体被复制,只能被引用赋值
noCopy noCopy
// 每个 P 的本地队列,实际类型为 [P]poolLocal
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
// [P]poolLocal的实际大小
localSize uintptr // size of the local array
// local from previous cycle
// Go 1.13 优化后添加的,相当于二级缓存,缓存上一次GC中local变量中存储的数据
victim unsafe.Pointer
// size of victims array
victimSize uintptr
// 自定义的对象创建回调函数,当 pool 中无可用对象时会调用此函数
New func() interface{}
}
// noCopy 用于嵌入一个结构体中来保证其第一次使用后不会被复制
type noCopy struct{}
// Lock 是一个空操作用来给 `go vet` 的 -copylocks 静态分析
// Error: copies lock value
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
因为 Pool 不希望被复制,所以结构体里有一个 noCopy 的字段,使用 go vet
工具可以检测到用户代码是否复制了 Pool。
noCopy 是 go1.7 开始引入的一个静态检查机制。它不仅仅工作在运行时或标准库,同时也对用户代码有效,用户只需实现这样的不消耗内存、仅用于静态分析的结构,来保证一个对象在第一次使用后不会发生复制
字段的具体解释:
local
字段存储指向 [P]poolLocal
数组(严格来说,它是一个切片)的指针,localSize
则表示 local 数组的大小。访问时,P 的 id 对应 [P]poolLocal
下标索引。通过这样的设计,多个 goroutine 使用同一个 Pool 时,减少了竞争,提升了性能。
在一轮 GC 到来时,victim 和 victimSize 会分别“接管” local 和 localSize。victim
的机制用于减少 GC 后冷启动导致的性能抖动,让分配对象更平滑。
引入了 victim
(二级)缓存,每次 GC 周期不再清理所有的缓存对象,而是将 local
中的对象暂时放入 victim
,从而延迟到下一个 GC 周期进行回收;
在下一个周期到来前,victim
中的缓存对象可能会被偷取,在 Put 操作后又重新回到 local
中,这个过程发生在从其他 P 的 shared
队列中偷取不到、以及 New 一个新对象之前,进而是在牺牲了 New 新对象的速度的情况下换取的;
Victim Cache 本来是计算机架构里面的一个概念,是 CPU 硬件处理缓存的一种技术,sync.Pool 引入的意图在于降低