简单介绍
通常在数据库连接、网络连接或其他具有持久性和资源消耗较高的资源下,我们会使用 Connection Pool(连接池)
降低建立和关闭连接的成本,并确保在需要时可以快速获取可用的连接。
同样在并发编程中,资源的分配和回收是一个很重要的问题,对于常用资源频繁的分配和回收,会造成大量性能的开销,sync.Pool
是 Go 语言标准库 sync
中提供的一个用于管理临时对象的 对象池,通过池化技术缓存和复用分配的临时对象,避免频繁地创建和销毁,减少程序GC的压力,以提升程序的性能。
由于 sync.Pool 是并发安全的,所以多个 goroutine 可以同时访问同一个 sync.Pool 对象,从而共享池中的对象,避免竞争条件
源码分析
基础结构

Go
type Pool struct {
noCopy noCopy // 防拷贝标识
local unsafe.Pointer // localPool,存储着各个 P 对应的本地对象池
localSize uintptr // 数组大小,等于 p 的个数
victim unsafe.Pointer // 经过一轮 gc 之后,暂存上一轮的 localPool
victimSize uintptr // 数组大小,等于 p 的个数
New func() any
}
Go
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
// Local per-P Pool appendix.
type poolLocalInternal struct {
private any // P 的私有元素,操作时无需加锁
shared poolChain // P 的共享元素链表
}
核心方法
pool.pin()
;将当前 goroutine 与 P 进行绑定,短暂处于不可抢占状态,以支持无锁化获取 private
中的元素
Go
func (p *Pool) pin() (*poolLocal, int) {
// 取出当前 P 的 index
pid := runtime_procPin()
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local // load-consume
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
// 如果是首次调用 pin 方法,则会走进 pinSlow 方法
return p.pinSlow()
}
pool.Get()
;从池中选择任意项目,将其从池中删除,然后将其返回给调用者
Go
func (p *Pool) Get() any {
if race.Enabled {
race.Disable()
}
// 绑定
l, pid := p.pin()
// 获取 private 的值,并将其置为 nil
x := l.private
l.private = nil
if x == nil {
// 如果 private 中没有值,从 shared 的头部获取
x, _ = l.shared.popHead()
if x == nil {
// 从其他 poolLocal 的 shared 里面偷取 --> 在 victim 中再执行一轮操作
x = p.getSlow(pid)
}
}
// Unpin()
runtime_procUnpin()
// 是否启用了数据竞争检测,默认为 false
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
// 如果 Get 返回 nil,而 p.New 不是 nil,则 Get 返回调用 p.New()
if x == nil && p.New != nil {
x = p.New()
}
return x
}
pool.Put()
;将取出的对象放回池中
Go
func (p *Pool) Put(x any) {
if x == nil {
return
}
if race.Enabled {
if fastrandn(4) == 0 {
// Randomly drop x on floor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
l, _ := p.pin()
if l.private == nil {
// 如果当前 poolLocal 的 private 是空的,直接放里面
l.private = x
} else {
// 否则加到 shared 头部
l.shared.pushHead(x)
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
垃圾回收
存入 Pool 的对象会不定期被 Go 运行时回收,最多两轮 GC,Pool 内的对象资源将会全部回收,因此即便大量存入元素,也不会发生内存泄露
但是需要持久化的东西最好还是不要放进去,比如数据库连接
Go
func poolCleanup() {
// 遍历 pools,回收 victim 中资源
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// 将 local 中资源转移到 victim 中
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
使用示例
下面是一个简单的 sync.Pool
使用示例;在创建 sync.Pool
时,需要传入一个 New()
,当 Get()
方法获取不到对象时,此时将会调用 New()
创建新的对象返回
Go
type Worker struct {
Name string
}
func main() {
pool := sync.Pool{New: func() any {
fmt.Printf("create a new worker----------> ")
return &Worker{Name: "zhang3"}
}}
w1 := pool.Get().(*Worker)
fmt.Println("1", w1)
pool.Put(w1)
w1 = pool.Get().(*Worker)
fmt.Println("2", w1)
w2 := pool.Get().(*Worker)
fmt.Println("3", w2)
pool.Put(w1)
pool.Put(w2)
}
// create a new worker----------> 1 &{zhang3}
// 2 &{zhang3}
// create a new worker----------> 3 &{zhang3}
可以看到,第一次获取对象时,New()
被调用,创建了一个新的对象;然后,我们将对象归还到池中,第二次获取对象,这时应该从池中获取,而不是创建新的对象;第三次没有将之前的对象放回池中,所有又再次创建了新的对象。
工程实践
Context
对象常用来跟踪请求的上下文信息,频繁地创建和销毁可能会产生一定的性能开销。
在 web 框架 gin 中,定义了一个 gin.Context
来跟踪一个 http 请求,串联前后处理函数,并传递相关信息。gin.Context
作为一个固定的结构,并且每一次请求来了都需要创建,请求结束之后再进行销毁;那么一旦大量请求打入,可能会面临较大的 GC 压力,因此 gin 使用 sync.Pool
来复用 Context
。
Go
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
}
engine.RouterGroup.engine = engine
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
func (engine *Engine) allocateContext(maxParams uint16) *Context {
v := make(Params, 0, maxParams)
skippedNodes := make([]skippedNode, 0, engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}