sync.pool使用介绍
背景
正常情况下,一个函数里创建一个对象时会分配新的内存空间,假如这个函数被频繁调用,那么就会频繁地创建新对象,分配新的内存空间。如果这个变量发生逃逸,那就会对GC产生比较大的负担,因此我们自然而然地就会思考:对象能不能复用?即创建对象并使用完后再次被使用,同时也考考虑到多个协程同时复用一个对象的场景。
sync.pool
就是为了解决上述问题。
示例
package main
import (
"runtime"
"sync"
"time"
)
func main() {
runtime.GOMAXPROCS(2)
var pool = sync.Pool{
New: func() interface{} {
return 0
},
}
go func() {
pool.Put(1) // 放到私有对象里
// pool.Put(2) // 放到共享池里
// pool.Put(3) // 追加到共享池里
}()
time.Sleep(1 * time.Second)
go func() {
// 如果本地私有对象和共享池里都没有,从其他协程对应P的共享池中获取
println(pool.Get().(int))
}()
time.Sleep(1 * time.Second)
}
这里起了两个协程,协程1放入了一个对象到私有对象里,协程2有一半的几率获取到0, 一半的几率获取到1。
接下来介绍一下sync.pool
的底层原理:
结构体
Pool
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
New func() any
}
noCopy
:和sync.WaitGroup
中的结构体一样,也是禁止拷贝;
local
:是个数组,长度为 P 的个数。其元素类型是poolLocal
,可以近似地看做[P]poolLocal
;
localSize
:P的个数;
victim
:一次GC后并不会把对象清除,而是会把local
里的数据拷贝到这里, 这样做是为了防止 GC 之后 sync.Pool 被突然清空,对程序性能造成影响。如果在其他的P中取不到数据,最后也可以从 victim 中取,这样程序性能会更加平滑;
victimSize
:和victim
一起使用;
New
:一个保底的方法,在获取对象是,依次从本地P的私有对象->本地P的共享池->其他P的共享池->victim里获取,如果都没有取到(比如多个协程同时取),则调用该方法创建一个。
poolLocal
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
每个P维护一个poolLocal
poolLocalInternal
type poolLocalInternal struct {
private any // Can be used only by the respective P.
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
private
:私有对象;
shared
:共享池,是一个双向链表。
poolChain
type poolChain struct {
head *poolChainElt
tail *poolChainElt
}
poolChainElt
type poolChainElt struct {
poolDequeue
next, prev *poolChainElt
}
最终的结构图如下:
介绍完结构体后再看看它实现的几个方法:
Put()
func (p *Pool) Put(x any) {
if x == nil {
return
}
// ...
l, _ := p.pin()
if l.private == nil { // 如果私有值没有,则设置私有值
l.private = x
} else {
l.shared.pushHead(x) // 否则追加到该协程对应的共享池里
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
- 将当前goroutine钉在当前的P上(禁止抢占)并返回对应的
poolLocal
; - 如果自己的local里没有设置私有对象,则设置;
- 否则追加到local的共享池的头部;
- 解除禁止抢占。
再看看pin()
方法做了什么:
pin()
func (p *Pool) pin() (*poolLocal, int) {
// 将当前goroutine订到当前P上,并返回当前P的序号
pid := runtime_procPin()
// 获取当前的localSize
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local // load-consume
if uintptr(pid) < s {
// 如果pid大于等于localSize,则通过pid找到对应索引位置的poolLocal
return indexLocal(l, pid), pid
}
// 如果pid大于等于localSize,则需要创建新的poolLocal,来保证每个P都有一个poolLocal
return p.pinSlow()
}
- 先将当前goroutine订到当前P上,并返回当前P的序号;
- 获取当前的localSize,第一次put的时候localSize=0,则需要创建新的
poolLocal
,来保证每个P都有一个poolLocal,同时修改localSize
; - 如果pid的索引小于localSize,则直接返回
local
对应的索引位置的poolLocal
.
Get()
func (p *Pool) Get() any {
if race.Enabled {
race.Disable()
}
l, pid := p.pin()
x := l.private // 先从本地取
l.private = nil // 取完后删除
if x == nil {
x, _ = l.shared.popHead() // 如果本地没有,则从共享池里取
if x == nil {
x = p.getSlow(pid) // 如果共享池里没有则从其他协程偷取
}
}
runtime_procUnpin()
// ...
if x == nil && p.New != nil { // 如果都没取到,则调用New()方法生成一个
x = p.New()
}
return x
}
- 将当前goroutine钉在当前的P上(禁止抢占)并返回对应的
poolLocal
; - 先获取当前P中的私有对象,如果有则返回;
- 如果获取不到则从当前P的共享池中的尾部获取;
- 如果还获取不到,则从其他P的共享池中获取;
- 如果还获取不到,则从
victim
获取: - 解除禁止抢占;
- 此时如果还没有获取到,则调用
New
方法创建新的对象并返回。
下面看看getSlow()
是怎么从其他P获取对象的:
getSlow()
// 从其它P的共享池里获取对象
func (p *Pool) getSlow(pid int) any {
size := runtime_LoadAcquintptr(&p.localSize) // load-acquire
locals := p.local // load-consume
// 遍历locals列表,从其他local的共享池的尾部获取对象
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i+1)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
size = atomic.LoadUintptr(&p.victimSize)
if uintptr(pid) >= size {
return nil
}
// 如果从locals取不到则从victim取
locals = p.victim
l := indexLocal(locals, pid)
// 先从自己的私有对象里取
if x := l.private; x != nil {
l.private = nil
return x
}
// 再从其他的共享池的尾部取
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// 取不到则将victimSize置位0,下次就不会再从victim中取了
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
- 将locals置为p.local;
- 遍历locals中所有的
poolLocal
,如果是跟自己绑定的P的poolLocal
,则获它的取私有对象; - 如果获取不到,则从共享池的尾部获取;
- 如果还是获取不到,则将locals置为p.victim,并重复步骤2、3
- 如果还是取不到,则将
victimSize
置为0,下次就不会再从victim中取了(下次GC之前)。
总结
总结来说,sync.Pool 利用以下手段将程序性能做到了极致:
- 利用 GMP 的特性,为每个 P 创建了一个本地对象池 poolLocal,尽量减少并发冲突;
- 每个 poolLocal 都有一个 private 对象,优先存取 private 对象,可以避免进入复杂逻辑;
- 在 Get 和 Put 期间,利用
pin
锁定当前 P,防止 goroutine 被抢占,造成程序混乱; - 在获取对象期间,利用对象窃取的机制,从其他 P 的本地对象池以及 victim 中获取对象;
- 充分利用 CPU Cache 特性,提升程序性能。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性