基本介绍
- 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() any |
sync.Pool的构造函数,用于指定sync.Pool中缓存的数据类型,当调用Get方法时从对象池中获取对象时,若对象池中没有相应的对象,则用New方法创建一个新的对象 |
func (p *sync.Pool) Get() any |
从对象池中获取一个对象 |
func (p *sync.Pool) Put(x any) |
往对象池中放入对象,下次Get时复用 |
go
type Student struct{
Name string
Age int
}
func main() {
var pool sync.Pool
pool.New = func() any {
return &Student {
}
}
st:=pool.Get().(*Student)
st.Name="ccc"
st.Age=22
fmt.Println(st)
fmt.Printf("addr is %p\n",st)
// st.Name=""
// st.Age=0
pool.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结构
go
type Pool struct {
noCopy noCopy // 用于防止 Pool 结构体被意外拷贝的标记字段(通过编译期检查实现)
local unsafe.Pointer // 每个 P(逻辑处理器)专属的固定大小本地对象池,实际类型为 [P]poolLocal
localSize 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
go
// 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 = nil
if 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 缓存数组的范围,说明无对应缓存,返回 nil
return 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方法
go
// 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上有对象,将对象推入链表的头部