Golang - sync.Pool底层源码详解
sync.Pool 是 sync 包下的一个组件,用来提高对象复用几率,减少gc的压力,减少内存分配,它是并发安全的,常用来存储并复用临时对象。
任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩。
可伸缩的,其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器。
sync.Pool中的值只在两次GC中间的时段有效。
原理
为了减小并发中锁的竞争,sync.pool为每个P(对象cpu线程)分配一个子池子poolLocal,每个poolLocal有private对象和shared共享列表对象,private私有对象只有对应的P可访问,无需加锁, shared共享列表对象可被其它P共享,需要加锁。
sync.pool结构体
type Pool struct {
noCopy noCopy //该对象不能被copy使用
local unsafe.Pointer // [p]poolLocal,固定长度
localSize uintptr //本地缓冲池poolLocal的数量
New func() interface{} //用户自定义的用于生成对象的方法【当池中没有可用对象时,会调用 New 函数构造构造一个对象】
}
sync.pool的结构组成如上图所示,会有两个疑问:
1)、实例化 Sync.Pool的时候,为什么实例化了一个LocalPool数组,怎么确定我的数据应该存储在LocalPool数组的哪个单元?
这里的LocalPool是根据不同的pid来区分的,保证private数据的线程安全,程序运行的时候可以获取到pid,然后使用pid作为LocalPool的索引,找到对应的地址即可
2)、PoolLocalInternal 里面的成员有private和shared,为什么要做这两种区分?
private 是 P 专属的, shared是可以被其他的P获取到的(类似于GMP模型对G的抢占)
Get函数
作用:从Pool中获取一个对象,如果获取不到并且New函数不为空,则通过New创建一个对象并返回;如果 New 未设置,则返回 nil。
源码:
Pool 会为每个 P 维护一个本地池,P 的本地池分为 私有池 private 和共享池 shared。私有池中的元素只能本地 P 使用,共享池中的元素可能会被其他 P 偷走,所以使用私有池 private 时不用加锁,而使用共享池 shared 时需加锁。
Get 会优先查找本地 private,再查找本地 shared,最后查找其他 P 的 shared,如果以上全部没有可用元素,最后会调用 New 函数获取新元素。

func (p *Pool) Get() interface{} {
if race.Enabled {
race.Disable()
}
//获取当前线程的poolLocal
l := p.pin()
//如果private对象不为空则直接返回,并将其置为nil
x := l.private
l.private = nil
runtime_procUnpin()
if x == nil {
//private不存在则加锁从shared列表中拿
l.Lock()
last := len(l.shared) - 1
if last >= 0 {
x = l.shared[last]
l.shared = l.shared[:last]
}
l.Unlock()
//如果shared对象列表依然没有的话,则从其它P的poolLocal获取
if x == nil {
x = p.getSlow()
}
}
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
// 如果存在New func回调函数,则执行
if x == nil && p.New != nil {
x = p.New()
}
return x
}
Put()函数
作用:将对象x放入到Pool中,以便复用该对象。
操作逻辑:
1)获取当前执行的Pid
2)根据Pid,找到对应的PoolLocal(数组),接着使用里面PoolLocalInternal(结构体)
2)优先存入 PoolLocalInternal 的 private属性,然后存入PoolLocalInternal 的 shared (slice)里面
源码:
Put 优先把元素放在 private 池中;如果 private 不为空,则放在 shared 池中。有趣的是,在入池之前,该元素有 1/4 可能被丢掉。

func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
if race.Enabled {
if fastrand()%4 == 0 {
// Randomly drop x on floor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
//如果当前poolLocal的private对象为nil,则直接赋值
l := p.pin()
if l.private == nil {
l.private = x
x = nil
}
runtime_procUnpin()
//否则加到当前poolLocal的shared列表中
if x != nil {
l.Lock()
l.shared = append(l.shared, x)
l.Unlock()
}
if race.Enabled {
race.Enable()
}
}
poolCleanup回收对象
作用:用来清理Pool,但官方的实现略显粗暴。
这个函数会在GC之前调用,这也就解释了官方的下面一句话:
Any item stored in the Pool may be removed automatically at any time without
notification. If the Pool holds the only reference when this happens, the
item might be deallocated.
//存储在池中的任何项目都可以随时自动删除,无需通知。如果发生这种情况时 Pool 持有唯一的引用,则项目可能会被取消分配。
如果一个数据仅仅在Pool中有引用,那么就需要担心这个数据被GC清理掉。
源码:
当垃圾回收(STW)将要开始时, poolCleanup 会被调用。
该函数内不能分配内存且不能调用任何运行时函数。原因:
1)防止错误,保留整个 Pool;
2)如果 GC 发生时,某个 goroutine 正在访问 l.shared,整个 Pool 将会保留,下次执行时将会有双倍内存。

func init() {
runtime_registerPoolCleanup(poolCleanup)
}
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
return (*poolLocal)(lp)
}
// Implemented in runtime.
func runtime_registerPoolCleanup(cleanup func())
func runtime_procPin() int
func runtime_procUnpin()
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.
// Defensively zero out everything, 2 reasons:
// 1. To prevent false retention of whole Pools.
// 2. If GC happens while a goroutine works with l.shared in Put/Get,
// it will retain whole Pool. So next cycle memory consumption would be doubled.
for i, p := range allPools {
allPools[i] = nil
for i := 0; i < int(p.localSize); i++ {
l := indexLocal(p.local, i)
l.private = nil
for j := range l.shared {
l.shared[j] = nil
}
l.shared = nil
}
p.local = nil
p.localSize = 0
}
allPools = []*Pool{}
}
可以看到在init的时候注册了一个PoolCleanup函数,它会清除掉sync.Pool中的所有的缓存的对象,这个注册函数会在每次GC的时候运行,所以sync.Pool中的值只在两次GC中间的时段有效。
案例1:gin 中的 Context pool
在 web 应用中,后台在处理用户的每条请求时都会为当前请求创建一个上下文环境 Context,用于存储请求信息及相应信息等。Context 满足长生命周期的特点,且用户请求也是属于并发环境,所以对于线程安全的 Pool 非常适合用来维护 Context 的临时对象池。
Gin 在结构体 Engine 中定义了一个 pool:
type Engine struct {
// ... 省略了其他字段
pool sync.Pool
}
案例2:fmt 中的 printer pool
fmt.Printf()的调用是非常频繁的,利用 sync.Pool 复用 pp 对象能够极大地提升性能,减少内存占用,同时降低 GC 压力。
printer 也符合长生命周期的特点,同时也会可能会在多 goroutine 中使用,所以也适合使用 pool 来维护。
printer 与 它的临时对象池:
// pp 用来维护 printer 的状态
// 它通过 sync.Pool 来重用,避免申请内存
type pp struct {
//... 字段已省略
}
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
总结:
Get方法并不会对获取到的对象值做任何的保证,因为放入本地池中的值有可能会在任何时候被删除,但是不通知调用者;放入共享池中的值有可能被其他的goroutine偷走。
所以对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,因为存入对象池重的值有可能会在垃圾回收时被删除掉,这违反了数据库连接池建立的初衷。
根据上面的说法,Golang的对象池严格意义上来说是一个临时的对象池,适用于储存一些会在goroutine间分享的临时对象。主要作用是减少GC,提高性能。在Golang中最常见的使用场景是fmt包中的输出缓冲区。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10亿数据,如何做迁移?
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 易语言 —— 开山篇
· Trae初体验