go语言的sync.pool

前言

后端性能优化存在着经典的三板斧——异步、分治、复用.

  • 异步:例如同步任务转异步,实现流程解耦,类似于 MQ(Message Queue,消息队列)的设计理念 ;例如在批处理过程中,先进行异步并发处理,最后再对结果聚合汇总,类似于 go 中 sync.WaitGroup 的设计理念;
  • 分治:针对高强度任务,可以化整为零,把工作量分治到多个预处理操作中,将蜂屯蚁聚化解为细水长流,类似于 go 中 map 渐进式扩容的设计理念;
  • 复用:对于高频使用的资源类型进行循环利用,而非重复创建销毁,类似于池化技术的设计理念.

今天要介绍的就是复用,也就是所谓的对象池。

在go语言也有着这样一个对象池sync.Pool,他就是池化计数应用落地的经典案例,在正式开启今天课程之前呢,先抛出一个引子:

试想一个场景,在 go 程序中,我们需要在短时间内并发构造大量的对象实例,此时由于内存分配行为导致内存使用率激增,进一步增加了 GC(Garbage collection,垃圾回收)的执行频率,频繁的 GC 又进一步限制了程序的执行效率,使得内存释放效率更低,最终一步步形成恶性循环.

在上述场景中,已知有大量对象实例在短时间内被重复构造与回收,倘若我们有一个资源回收中转站,对不用的实例进行暂存而非直接销毁,并提供循环复用的能力而非无脑新建实例,那么就能在很大程度上降低内存分配和垃圾回收的压力,进而优化整个程序的执行效率.

对象池 sync.Pool 正是扮演了这个资源回收中转站的角色. 通过这种方式,sync.Pool 能在一定程度上预测对象的生命周期,在对象可能被回收之前,给予它们一个被重复使用的机会

一.Pool实例的创建和使用

1.1 创建一个pool实例

Pool 池的模式是通用型的(存储对象的类型为interface{}),所有的类型的对象都可以进行使用。

注意的是,作为使用方不能对 Pool 里面的对象个数做假定,同时也无法获取 Pool 池中对象个数。

// 创建一个 Pool 用于缓存 bytes.Buffer 对象
var bufferPool = sync.Pool{New: func() interface{} {// 当 Pool 中没有可用对象时,调用 New 函数生成新对象return new(bytes.Buffer)},
}

1.2 Put和Get接口

  • Put() interface{}:将一个对象加入到 Pool 池中 注意的是这仅仅是把对象放入池子,池子中的对象真正释放的时机是不受外部控制的。
  • Get() interface{}:返回 Pool 池中存在的对象
    注意的是 Get() 方法是随机取出对象,无法保证以固定的顺序获取 Pool 池中存储的对象。
// 从 Pool 中获取一个 bytes.Buffer(如果 Pool 为空,会自动调用 New 函数创建)
buf := bufferPool.Get().(*bytes.Buffer)// 处理完成后将对象放回 Pool
defer bufferPool.Put(buf) 

1.3 性能测试

可以写两个基准测试,测试一下两个的性能,看下结果可以发现,它的时间消耗有了明显的下降

也就是说,如果创建一个对象比较耗时或者消耗内存的化,使用Pool的化,会明显提升性能和内存消耗。

package syncPoolimport ("bytes""io/ioutil""sync""testing""time"
)var pool = sync.Pool{New: func() interface{} {return newBytesBuffer()},
}
var fileName = "test_sync_pool.log"
var data = make([]byte, 10000)func newBytesBuffer() *bytes.Buffer {time.Sleep(time.Millisecond)return new(bytes.Buffer)
}func BenchmarkWriteFile(b *testing.B) {for n := 0; n < b.N; n++ {buf := newBytesBuffer()buf.Reset() // Reset 缓存区,不然会连接上次调用时保存在缓存区里的内容buf.Write(data)_ = ioutil.WriteFile(fileName, buf.Bytes(), 0644)}
}func BenchmarkWriteFileWithPool(b *testing.B) {for n := 0; n < b.N; n++ {buf := pool.Get().(*bytes.Buffer) // 如果是第一个调用,则创建一个缓冲区buf.Reset() // Reset 缓存区,不然会连接上次调用时保存在缓存区里的内容buf.Write(data)_ = ioutil.WriteFile(fileName, buf.Bytes(), 0644)pool.Put(buf) // 将缓冲区放回 sync.Pool中}
}
goos: darwin
goarch: amd64
pkg: test/utils/syncPool
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkWriteFile-12                        840           1539251 ns/op           10368 B/op          4 allocs/op
BenchmarkWriteFileWithPool-12               6866            174601 ns/op             129 B/op          3 allocs/op
PASS
ok      test/utils/syncPool     3.951s

池子的创建和使用是很简单的,但是为什么要使用pool,以及它的底层是什么?什么时候应该使用他呢?

这都是我们接下来要学习的内容

二.Pool的介绍

在正式介绍之前,先了解以下内容

为什么要使用Pool呢?

  1. 减少内存分配开销
    频繁创建和销毁对象会触发垃圾回收(GC),而 sync.Pool 通过缓存和重用对象,减少了内存分配次数,从而降低 GC 压力。
  2. 提升性能
    重用对象比反复分配内存更快,尤其是在处理大量临时对象时(如 HTTP 请求解析、编解码缓冲区)。
  3. 适合高频临时对象
    对于生命周期短且频繁创建的对象(如 bytes.Buffer、临时结构体),池化能显著优化性能。
  4. 自动管理,GC 友好
    sync.Pool 中的对象可能被垃圾回收器自动清理(默认策略),避免长期占用内存。

注意事项:

  1. 对象可能被回收
    sync.Pool 中的对象不保证长期存在,GC 运行时可能被清理,因此不可用于存储持久化数据(如数据库连接)。
  2. 适合无状态对象
    池中的对象需在放回前重置状态(如调用 Buffer.Reset()),避免脏数据。
  3. 并非万能
    如果对象创建成本低,或使用频率不高,强行池化反而会增加复杂度。

使用场景

使用 sync.Pool 的场景

  • 需要频繁创建/销毁临时对象。
  • 对象初始化成本较高(如含内存分配或系统调用)。
  • 希望减少 GC 压力,提升高并发下的性能。

避免使用的场景

  • 对象有状态或需长期持有。
  • 对象复用带来的收益小于池化管理的复杂度。

看看那些具体使用到对象池的例子吧

在实际工程场景中,sync.Pool 有着十分广泛的应用,比如 go 著名的 web 框架——gin 中,就对 sync.Pool 有所应用:

在基于 gin 启动的 http server 中,针对到来的 http 请求,会为之分配一个 gin.Context 实例,由于承载关于这次请求链路的上下文信息.

在这个场景中,gin.Context 就是一个可能被量产使用的工具类,其本身创建销毁成本不高,但随着 qps(Query Per-Second) 的增长,可能在短时间内被重复创建、销毁,因此很适合使用对象池技术进行缓存复用.

下面简单展示 gin 中对 sync.Pool 的使用细节,对应代码位于 gin.go 文件,使用的 gin 源码版本为 v1.9.1:

// 构建 gin.Engin 实例,其属于 http Handler 实现类
func New() *Engine {// ...engine := &Engine{// ...}// .../*engine.pool 就是对象池,用于托管复用 gin.Context 此处声明了 pool 中针对 gin.Context 实例的构造函数*/engine.pool.New = func() any {return engine.allocateContext(engine.maxParams)}return engine
}// 处理 http 请求
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {//  从对象池中获取 gin.Context 实例c := engine.pool.Get().(*Context)// ...// 清空 gin.Context 实例中的内容c.reset()// 处理 http 请求engine.handleHTTPRequest(c)// 用完后,将 gin.Context 实例归还到对象池中engine.pool.Put(c)
}

除了 gin 之外,像是 fmt 标准库中针对 printer pp,协程池三方库 ants 中针对协程对象 goworker,都应用到了 sync.Pool 技术进行管理复用,大家若感兴趣也可以自行探索求证.

2.1 实现原理

下面揭示关于 sync.Pool 的底层实现原理,我们知道,在 go 中,gmp 架构是一切技术理论的基石:

sync.Pool 底层的存储结构设计同样与 gmp 架构息息相关,通过为每个处理器 P 分配一个存储容器,其中包含 P 私有的对象实例 private 和全局共享对象列表 shared,来实现资源的统筹分配,并最大化降低并发粒度.

结合上图,我们进一步对 sync.Pool 的核心设计要点进行阐述:

  • 为每个处理器 P 分配一私(private)一公(shared)两部分存储介质;
  • private 是 P 私有的对象实例,当前 P 可以完全无锁化访问 private. 无论是 Get 还是 Put 流程,都优先使用 P 的 private 资源;
  • shared 是 P 下的共享对象实例列表,当前 P 访问 shared 时也需要通过 CAS(Compare-And-Swap) 操作保证并发安全,这是因为其他 P 可能也会尝试来此窃取资源,因此存在并发行为.

2.2 Pool底层结构(后续在了解吧)

type Pool struct {  // 用于检测 Pool 池是否被 copy,因为 Pool 不希望被 copy。用这个字段可以在 go vet 工具中检测出被 copy(在编译期间就发现问题) noCopy noCopy  // A Pool must not be copied after first use.// 实际指向 [P]poolLocal,数组大小等于 P 的数量;每个 P 一一对应一个 poolLocallocal     unsafe.Pointer     // local fixed-size per-P pool, actual type is [P]poolLocallocalSize uintptr      		// size of the local array,[P]poolLocal 的大小// GC 时,victim 和 victimSize 会分别接管 local 和 localSize;// victim 的目的是为了减少 GC 后冷启动导致的性能抖动,让分配对象更平滑;victim     unsafe.Pointer victimSize uintptr       // 对象初始化构造方法,使用方定义New func() interface{}
}

https://zhuanlan.zhihu.com/p/616436531