前面一篇讲解了Sync.Pool的底层数据结构 poolDequeue,接着看看Sync.Pool
的具体实现原理。如果想看看 Sync.Pool
的使用 可以看看我的 Go 入门 26-Issue,使用的时候最好分配固定大小的对象否则注意清理
带着问题看世界
Sync.Pool
与Goroutine
的关系Sync.Pool
是如何释放的(并没有主动释放接口)
代码:src/sync/pool.go
,1.20版本
首先来看内存池的说明,翻译过来就是就是:
- 存放在
Pool
中的元素任何时候都有可能在没有被其他引用的情况下释放掉 Pool
是并发安全的- 使用
Pool
之后不能再复制它。假设缓存池对象 A 被对象 B 拷贝了,如果 A 被清空,B 的缓存对象指针指向的对象将会不可控
先来看看整体的结构图
全局变量
1 | var poolRaceHash [128]uint64 |
allPoolsMu
:用于对allPools
的更新进行保护allPools
:[]*Pool
切片,存储具有非空私有缓存的对象池。可以被多个goroutine
访问oldPools
:[]*Pool
类型的切片,用于存储可能具有非空受害者缓存的对象池,由于只有在STW
时候才会更新,不会被并发访问
基本结构
1 | type Pool struct { |
-
noCopy
用于提示不要进行对象复制 -
local
与victim
的关系在后面 内存池清理 中说明 -
New
则是自定义的分配对象函数
noCopy
noCopy
支持 使用 go vet
检查对象是否被复制,它是一个内置的空结构体类型,当然也可以自行实现类似功能
1 | type noCopy struct{} |
它第一次使用后不能被复制,其实代码是能够编译通过运行的,只是在 go vet
或者 部分编辑器
会提示而已。可以看看没有成为标准的原因 https://golang.org/issues/8005#issuecomment-190753527
poolLocal
每个处理器 P
都有一个 poolLocal
的本地池对象
1 | // 每个 P 的本地对象池 |
private
是一个仅用于当前 P 进行读写的字段(即没有并发读写的问题)shared
可以在多个 P 之间进行共享读写,是一个poolChain
链式队列结构, 当前 P 上可以进行pushHead
和popHead
操作(队头读写), 在所有 P 上都可以进行popTail
(队尾出队)操作pad
用于 伪共享 保证poolLocal
的大小是 128 字节的倍数
runtime_procPin
1 | //go:nosplit |
locks
:通过增加锁的计数,表明当前线程被固定(pinned)在处理器上mp.p
表示当前线程所绑定的处理器,.ptr()
方法返回处理器的指针,.id
表示处理器的唯一标识符
pinSlow
将当前的goroutine
绑定到Pool
中的一个poolLocal
上,并返回该poolLocal
及其索引
1 | func (p *Pool) pinSlow() (*poolLocal, int) { |
这个逻辑的前提是当前 P 已经发生了动态调整,需要重新计算localPool
- 首先解除
goroutine
与Process
的绑定,让goroutine
可以重新绑定 - 获取
allPools
的全局锁 - 将当前
goroutine
与Process
重新绑定 - 如果
Process
未发生变化,返回Process
的localPool
- 如果没有变化则
- 如果
Pool.local
为空,则需要将Pool
加入到allPools
中,用于 GC 扫描回收 - 重新创建
[p]poolLocal
- 重新将
Pool.local
指向[p]poolLocal
- 如果
pin
获取当前 Process
中的 poolLocal
,将当前的goroutine
绑定到一个特定的Process
上,禁用抢占并返回Process
的poolLocal
本地池和P
的标识
1 | func (p *Pool) pin() (*poolLocal, int) { |
尝试通过加载local
和localSize
字段的方式来判断是否可以直接返回一个可用的poolLocal
,如果不满足条件,则调用pinSlow
方法来重新分配并返回一个新的poolLocal
。调用者在使用完poolLocal
之后,必须调用runtime_procUnpin()
来解除与P
的绑定关系。
1 | func indexLocal(l unsafe.Pointer, i int) *poolLocal { |
内存池清理
在使用 init 仅执行了一个逻辑,就是注册内存池回收机制
1 | func init() { |
poolCleanup
用于实现内存池的清理
1 | func poolCleanup() { |
垃圾回收的策略就是
- 将
oldPools
中也就是所有localPool
的victim
对象丢弃 - 将
allPools
的local
复制给victim
,并local
重置 - 最后将
allPools
复制给oldPools
,allPools
置空
Get
整体流程如下

- 首先获取当前
Process
的poolLocal
,也就是说当 Goroutine 在哪个 Process 运行的时候就会从哪个 Process 的 localPool中获取对象 - 优先从
private
中选择对象,并将private = nil
- 若取不到,则尝试从
shared
队列的队头进行读取 - 若取不到,则尝试从其他的
Process
中进行偷取getSlow
(跨 Process 读写) - 若还是取不到,则使用自定义的
New
方法新建对象
获取对象代码如下操作
1 | func (p *Pool) Get() any { |
其中的 getSlow
就是从 其他 Process
或者 victim
中获取
1 | func (p *Pool) getSlow(pid int) any { |
Put
Put 的操作如下

存放策略是:
- 如果存放
nil
直接返回 - 获取当前
Process
的poolLocal
- 如果
private == nil
则放到private
中 - 如果
private != nil
则将起放入到 链表头部
1 | func (p *Pool) Put(x any) { |
总结
- Pool 本质是为了提高临时对象的复用率;
- Pool 使用两层回收策略(local + victim)避免性能波动;
- Pool 本质是一个杂货铺属性,啥都可以放,Pool 池本身不做限制;
- Pool 池里面 cache 对象也是分层的,一层层的 cache,取用方式从最热的数据到最冷的数据递进;
- Pool 是并发安全的,但是内部是无锁结构,原理是对每个 P 都分配 cache 数组(
poolLocalInternal
数组),这样 cache 结构就不会导致并发; - 永远不要 copy 一个 Pool,明确禁止,不然会导致内存泄露和程序并发逻辑错误;
- 代码编译之前用
go vet
做静态检查,能减少非常多的问题; - 每轮 GC 开始都会清理一把 Pool 里面 cache 的对象,注意流程是分两步,当前 Pool 池 local 数组里的元素交给 victim 数组句柄,victim 里面 cache 的元素全部清理。换句话说,引入 victim 机制之后,对象的缓存时间变成两个 GC 周期;
- 不要对 Pool 里面的对象做任何假定,有两种方案:要么就归还的时候 memset 对象之后,再调用
Pool.Put
,要么就Pool.Get
取出来的时候 memset 之后再使用;