sync.Pool
是 Go 并发原语中用于对象池化的工具,主要用于缓存和复用临时对象,以减少内存分配和垃圾回收的压力。
本文将带大家一起来深入探讨 sync.Pool
,包括使用示例和源码解读,让你彻底理解 sync.Pool
的设计。
简介
sync.Pool
核心功能是能够缓存对象,避免其缓存的对象在一定时间内被垃圾回收掉。
因此 sync.Pool
的主要作用也就体现了出来:减少内存分配和回收压力。
如果一个对象被频繁的创建和删除,那么对内存分配和 GC 压力就会比较大,使用 sync.Pool
能够将对象缓存到池中,避免频繁创建和删除对象。
sync.Pool
是一个结构体,其全部公开属性如下:
1 | type Pool |
每个属性含义如下:
New
字段:当池中没有可用对象时,调用New
函数创建一个新对象。Get
方法:从池中获取一个对象。如果池为空,则调用New
创建新对象。Put
方法:将对象放回池中,以便复用。
接下来我们一起来看下 sync.Pool
如何使用。
使用示例
sync.Pool
使用示例如下:
1 | package main |
这是 sync.Pool 官方文档中示例代码。
首先,在第 11 行直接通过 sync.Pool{}
语法实例化一个 Pool
对象 bufPool
,并且这里还初始化了 New
函数,其返回一个 *bytes.Buffer
对象。
接着,在第 25 行的 Log
函数内部使用了 bufPool
,通过 Get
方法得到一个 *bytes.Buffer
类型的对象 b
,然后向 b
中写入数据,最后别忘了使用 Put
方法将 *bytes.Buffer
对象“还回去”,将其缓存在池中,以便下次使用。
最后,在 main
函数中调用 Log
函数,并将结果写入标准输出。
执行示例代码,得到输出如下:
1 | $ go run main.go |
根据以上使用示例,我们可以总结 sync.Pool
使用套路:
- 实例化一个
sync.Pool
对象,并且赋值New
属性,用户构造缓存对象。 - 通过
p.Get()
取出对象obj
使用。- 如果池中有,就直接返回。
- 如果没有,调用
New
属性构造函数,构造一个新的对象并返回(如果没有New
属性,则返回nil
)。
- 对象使用完成后记得调用
p.Put(obj)
重新放入池中,以便下次使用。
由此可见,sync.Pool
适用于以下场景:
- 频繁创建和销毁的对象:如临时缓冲区。
- 减少内存分配:通过复用对象,减少 GC 压力。
- 无状态对象:池中的对象不应包含与特定上下文相关的状态。
此外,在使用 sync.Pool
时有两点需要我们特别注意:
- 对象重置:从池中获取的对象可能包含之前的状态,使用前需要重置。
- 对象生命周期:池中的对象可能会被 GC 回收,因此不能依赖池中的对象长期存在。
sync.Pool
的设计中有一个比较有意思的点,一个对象被放入池中以后,如果没被使用,则连续两次 GC 后,这个对象一定会被释放。
那么,你是否好奇,sync.Pool
内部是如何实现这一机制的呢?咱们接着往下看,我们一起通过源码来揭开 sync.Pool
的神秘面纱。
实现原理
学习了 sync.Pool
如何使用,接下来我们一起通过阅读源码的方式来深入到 sync.Pool
的原理学习。
sync.Pool 结构体
sync.Pool
是一个结构体,其定义如下:
1 | type Pool struct { |
其中 New
属性我们已经使用过了,调用 Get
方法时,如果缓存池中没有可用对象,则调用此方法生成一个新的值并返回。
noCopy
属性用来标记禁止复制,所以我们在拿到 sync.Pool
实例化对象后,记得一定不要让其产生复制操作。
sync.Pool
有两个核心字段分别是 local
和 victim
,二者都是 poolLocal
指针类型,用来存储缓存对象。local
是当前 P 本地缓存的对象,而 victim
则可以理解为 Windows 操作系统的“回收站”。
Go 在触发垃圾回收时,sync.Pool
会做两件事:
- 将所有缓存的
victim
中的对象移除。 - 把所有缓存的
local
中对象移动到victim
。
进入到 victim
中的对象最终会有两种结果:
- 当发生 GC 时,对象会被移除。
- 如果还未发生 GC,而是优先调用了
Get
方法,那么这个对象就会被重新使用。
所以说,victim
就是 Windows 电脑中的“回收站”,我们在电脑中删除文件时,先到回收站,然后在回收站里可以彻底删除。
poolLocal
同样是一个结构体,其定义如下:
1 | type poolLocalInternal struct { |
poolLocal
中包含了 poolLocalInternal
和 pad
两个属性。
其中 pad
属性并不是用来存放数据的,而是用于将 poolLocal
结构体所占用的内存对齐到 128 的整数倍。这是为了解决伪共享(false sharing
)问题,以此来独占 CPU 高速缓存的 CacheLine。
而 poolLocalInternal
结构体内部,才是用来存储缓存数据的。其中 private
是一个私有对象,用于记录当前 P 下缓存的对象,shared
是一个双向队列(一个 lock-free
的双向链表结构),用于记录多个 P 中共享的缓存对象,当前 P 能够进行 pushHead/popHead
操作,其他 P 能够进行 popTail
操作,从而在当前 P 中窃取缓存对象。
这里所说的 P 是指 Go GMP 模型中的处理器(P),之所以设计为当前 P 从队头进行读写,其他 P 从队尾进行获取操作,目的是在不加锁的情况下保证并发安全。
sync.Pool
为每个处理器(P)维护一个本地的 poolLocal
结构,其中包含一个 shared
队列。这个 shared
队列的类型是 poolChain
,它是一个由多个 poolDequeue
节点组成的双向链表结构。每个 poolDequeue
都是一个固定大小的环形队列(ring buffer
),并且每个新节点的容量通常是前一个节点的两倍。
poolDequeue
被设计为一个单生产者(single-producer
)/多消费者(multi-consumer
) 的无锁队列(lock-free
):
- 生产者:即当前
P
,可以执行pushHead
(在头部添加)和popHead
(从头部弹出)操作。 - 消费者:包括当前
P
(也可以消费)和其他P
。其他P
只能执行popTail
(从尾部弹出)操作。
对于 poolChain
的介绍就到这里,不再继续深入,避免陷入其中,我们应该继续回到 sync.Pool
本身方法的学习。
Put 方法
Put
方法用于添加一个对象到池中,其实现如下:
1 | // Put 添加一个元素到池中 |
可以发现,Put
方法实现逻辑相当简单。
其中 p.pin()
和 runtime_procUnpin()
是必须成对出现的调用,有点类似互斥锁的加锁/解锁操作,并且同样是用来解决并发问题的。不同的是,pin
操作更加轻量,p.pin()
能够将当前 goroutine 固定在当前的 P 上。因为在一个 P 上,同一时刻只会运行一个 goroutine,所以,接下来在当前 goroutine 中操作当前 P 上的任何对象都无需加锁,从而避免的并发问题。
调用 p.pin()
能够拿到存储在当前 P 中的 *poolLocal
对象和当前 P ID
,有了 *poolLocal
对象,就可以判断 l.private
是否为空,如果值为 nil
,那么直接将对象 x
赋值到 private
属性中缓存起来。否则,将对象 x
存储到共享队列 l.shared
中。
最后,记得调用 runtime_procUnpin()
解除 goroutine 和 P 的绑定。
Get 方法
Get
方法用于从池中获取一个对象,其实现如下:
1 | // Get 从 [Pool] 中选择一个任意项,将其从 Pool 中移除,然后返回给调用者。 |
与 Put
方法一样,Get
方法的逻辑也通过 p.pin()
和 runtime_procUnpin()
进行保护。
Get 方法在缓存中获取空闲对象的搜索路径如下:
- 从
l.private
中获取对象。 - 从本地共享队列
l.shared
中获取对象。 - 慢路径(尝试从其他 P 窃取或从
victim
回收站中获取)。
慢路径源码实现如下:
1 | func (p *Pool) getSlow(pid int) any { |
可以发现,getSlow
方法内部会逐个计算其他 P 的索引,然后从对应 P 的共享队列尾部 l.shared.popTail()
窃取缓存对象。
如果遍历完所有 P 的共享缓存,都没能找到缓存对象,则继续检查 victim
中的缓存数据。
如果 victim
的 private
中有数据,则直接返回,否则继续检查 victim
的共享队列,如果 victim
的共享队列中没能找到数据,最终才会返回 nil
。
现在 sync.Pool
实现缓存对象的主体逻辑已经串通了,但是还有一点没有串接起来,victim
是何时被赋值的?
pin 操作
要找到 victim
的赋值操作,还需要先理解 pin
方法的内部实现,其实现如下:
1 | // 将当前 goroutine 固定(pin)到其运行的 P(逻辑处理器)上,并返回该 P 对应的本地缓存池 (*poolLocal) 和 P 的 id |
可以看到,pin
操作也有快慢路径之分,慢路径 p.pinSlow()
一般出现在初始化场景中。
这里,我们需要重点关注的是如下这段代码:
1 | if p.local == nil { |
如果 Pool
尚未注册,即 p.local == nil
,则将其添加到 allPools
全局切片变量中,以便后续 GC 时能执行 poolCleanup
操作清理其缓存。
那么这个 allPools
是干什么的?我们接着往下看与 GC 相关的代码。
GC 垃圾回收
sync.Pool
中与 GC 相关的代码实现如下:
1 | //go:linkname poolCleanup |
这段代码要从下往上解读。
首先在 init
函数中 将 poolCleanup
注册到 runtime
,这样 poolCleanup
函数会在每次 GC 开始时自动被调用。
poolCleanup
函数内部会操作 allPools
和 oldPools
两个全局变量,因为是在 GC STW 时执行,不会存在并发问题,所以无需加锁。
而 poolCleanup
函数内的源码实现,正是我们在前文中所讲的,Go 在触发垃圾回收时,sync.Pool
会做两件事:
- 将所有缓存的
victim
中的对象移除。 - 把所有缓存的
local
中对象移动到victim
。
现在 victim
的赋值操作也找到了。当然,sync.Pool
的核心源码也随之解读完了。
sync.Pool 执行流
通过以上源码的分析,你可能还有些发懵,没关系,这是正常现象,我也是阅读了好几遍 sync.Pool
源码,才搞清楚其逻辑的。
如果你坚持阅读到这里,那么恭喜你,离真正理解 sync.Pool
更近了一步。
我们可以通过几张流程图,再来梳理一下 sync.Pool
的执行流,以此来加深对 sync.Pool
源码的理解。
Put
操作执行流程如下:

Get 操作执行流程如下:

至此,sync.Pool
原理就解读完了。
总结
本文带大家一起学习了 Go 并发原语 sync.Pool
,这是一个在并发场景下非常有效的解决对象复用的手段。
通过源码解读,我们知道 sync.Pool
默认缓存数据会存储在 local
中,在触发 GC 时则被移动到 victim
,victim
就像一个回收站,其内部的数据要被重新利用,要么被彻底删除。
你有没有想过,sync.Pool
为什么要设计成调用两次 GC 才会回收对象呢?
其实这是为了防止 GC 引起的性能抖动。如果只调用一次 GC,就回收对象,则可能导致对象被频繁的创建和回收,并不能有效起到缓存的作用。那如果调用 3 次 GC 再回收行不行呢?理论上可以,但不建议这样做,其实这是一个内存和性能之间的取舍问题,如果缓存数据没有被使用,还长期存放在内存中,则势必会造成内存的浪费。两次 GC 才回收对象,应该是一个比较合理的经验值。
本文示例源码我都放在了 GitHub 中,欢迎点击查看。
希望此文能对你有所启发。
延伸阅读
- sync.Pool Documentation:https://pkg.go.dev/sync@go1.25.0#Pool
- sync.Pool GitHub 源码:https://github.com/golang/go/blob/go1.25.1/src/sync/pool.go
- 10 | Pool:性能提升大杀器:https://time.geekbang.org/column/article/301716
- 本文 GitHub 示例代码:https://github.com/jianghushinian/blog-go-example/tree/main/sync/pool
- 本文永久地址:https://jianghushinian.cn/2025/09/07/sync-pool/
联系我
- 公众号:Go编程世界
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客:https://jianghushinian.cn
- GitHub:https://github.com/jianghushinian