golang系列之-sync.Pool
sync.Pool-临时对象池,是golang一个很关键的数据结构,通过复用历史对象,缓解因频繁创建、删除对象而导致的内存分配压力、GC压力,在社区中被广泛使用,有如go-gin、kubernetes等
当前go版本:1.24
快速上手
下面展示一个简单的使用示例,用于帮助用户快速上手
1 | package main |
数据结构
todo:文章图片待补充
sync.Pool的源代码注释被我删除了,建议自行查看源代码,简单总结如下
- sync.Pool用于临时对象服务存储/获取(临时对象可能随时被清理掉)
- sync.Pool是线程安全的
- sync.Pool使用后不应该也不能被复制
1 | // Pool -> local -> poolLocal_0 -> poolChainElt_0(head) -> poolDequeue(2^(3+N) |
注意:这里有几个全局变量用于纪录所有创建的池子
1 | var ( |
读写操作
辅助函数
Get和Put操作都依赖的几个公共方法放在这里,可以先看后续的读写代码再回头看这部份
- pin - 用于绑定goroutine和P,阻止进入抢占模式,并返回pid
- indexLocal - 获取local数组中指定的P的poolLocal索引
1 | // 返回poolLocal和id |
获取一个对象
大概逻辑如下
- 根据P的值定位poolLocal链表
- 如果private有数值,返回该值
- 从shared(head)获取一个数据
- 从所有P的poolLocal链表找数据(从下一个P开始)
- 从local查找 -> private -> shared(tail)
- 从victim查找(只使用一次) -> shared(tail)
- 使用New方法生成一个新对象
1 | func (p *Pool) Get() any { |
保存一个对象
大概逻辑如下
- 如果对象为nil,不处理
- 根据P的值定位poolLocal链表
- 如果private为nil,直接写入
- 如果private有数据,写入shared(head)
- shared链表为空,创建链表(初始数据量为8)
- 写入成功?返回
- 写入失败?扩容后再次写入(数据量最大不能超过2^30=1GB)
1 | // src/sync/pool.go |
数据过期
每当GC进入STW状态时,清理Pool相关数据
- Pool内数据过期:local -> victim & victim -> nil
- 全局变量数据过期:allPools -> oldPools & oldPools -> nil
1 | func poolCleanup() { |
存储切片注意
当使用sync.Pool存储切片时,sync.Pool会如何处理呢?看下面示例代码
1 | package main |
从上述代码看,我们创建了一个可以重复利用的切片/缓存区。打开Escape Analysis-逃逸分析运行看看
1 | go run -gcflags="-m" hello.go |
当创建一个slice-切片时,我们得到的是一个header,系统判断其不仅局限于New函数,使其逃逸至heap上分配,而b也是一个header,同样,系统也会使其逃逸至heap
重新调整代码,改为一个指向slice的指针,如下
1 | package main |
再一次运行逃逸分析,得到结果如下
1 | go run -gcflags="-m" hello.go |
这一次,把原始指针放回Pool就不会发生逃逸现象
参考文档
Let’s dive: a tour of sync.Pool internals
深度分析 Golang sync.Pool 底层原理
Go sync.Pool and the Mechanics Behind It