开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 22 天,点击查看活动详情
sync.Pool
sync.Pool
是Go语言提供的一个对象池实现,它可以用来缓存一些可重用的对象,以提高内存分配和垃圾回收的效率。
使用sync.Pool
可以避免一些反复分配和回收的操作,提高程序的性能和效率。使用sync.Pool
时需要注意以下几点:
- 对象池不是用来做对象缓存的,对象池里的对象会随时被删除,无法长时间存储。
- 对象池不能替代传统的对象缓存方案,例如Redis、Memcached等缓存方案。
- 对象池不能代替GC,它只是用来减少分配和回收操作的次数,以提高程序的性能和效率。
下面是一个使用sync.Pool
的例子,该例子创建一个对象池,用于缓存字符串切片对象,以提高字符串切片对象的分配和回收效率:
func main() {
// 创建一个对象池,用于缓存字符串切片对象
pool := &sync.Pool{
New: func() interface{} {
return make([]string, 0, 10)
},
}
// 从对象池中获取一个字符串切片对象
strs := pool.Get().([]string)
fmt.Printf("len=%d, cap=%d\n", len(strs), cap(strs))
// 往字符串切片中添加一些数据
strs = append(strs, "hello", "world")
fmt.Printf("len=%d, cap=%d, strs=%v\n", len(strs), cap(strs), strs)
// 把字符串切片放回对象池中
pool.Put(strs)
// 再次从对象池中获取一个字符串切片对象
strs = pool.Get().([]string)
fmt.Printf("len=%d, cap=%d, strs=%v\n", len(strs), cap(strs), strs)
}
pool字符串缓存的效率对比
func Benchmark_without_pool(b *testing.B) {
for i := 0; i < b.N; i++ {
// 新建字符串切片对象
strs := make([]string, 0, StrSize)
// 往字符串切片中添加一些数据
strs = append(strs, "hello", "world")
}
}
func Benchmark_pool(b *testing.B) {
// 创建一个对象池,用于缓存字符串切片对象
pool := &sync.Pool{
New: func() interface{} {
return make([]string, 0, StrSize)
},
}
for i := 0; i < b.N; i++ {
// 从对象池中获取一个字符串切片对象
strs := pool.Get().([]string)
// 往字符串切片中添加一些数据
strs = append(strs, "hello", "world")
// 把字符串切片放回对象池中
pool.Put(strs)
}
}
结果:
length | pool ns/op | without pool ns/op |
---|---|---|
10 | 139.1 | 0.2341 |
100 | 158.9 | 0.2374 |
1000 | 120.8 | 0.2295 |
10000 | 121.6 | 11716 |
100000 | 119.9 | 85328 |
1000000 | 103.2 | 1279465 |
可以看到字符串比较短时,go 运行时的string缓冲可以保证速度,但是当长度达到5000后,pool的优势就体现出来了,同时也可以看到,pool的基本调用,时间消耗约100ns。
源码阅读
type Pool struct {
noCopy noCopy
local unsafe.Pointer
localSize uintptr
victim unsafe.Pointer
victimSize uintptr
New func() any
}
- noCopy:用于检测Pool是否被复制
- local是每个P(处理器)的固定大小池的指针,实际类型为[P]poolLocal。
- localSize是本地数组的大小。
- victim是上一个周期的本地池的指针。
- victimSize是victim数组的大小。
- New可选地指定一个函数,以在Get返回nil时生成一个值。 它不能与调用Get并发地更改。
每次垃圾回收的时候,Pool 会把 victim 中的对象移除,然后把 local 的数据给 victim,这样的话,local 就会被清空,而 victim 就像一个垃圾分拣站,里面的东西可能会被当做垃圾丢弃了,但是里面有用的东西也可能被捡回来重新使用。
victim 中的元素如果被 Get 取走,那么这个元素就很幸运,因为它又“活”过来了。但是,如果这个时候 Get 的并发不是很大,元素没有被 Get 取走,那么就会被移除掉,因为没有别人引用它的话,就会被垃圾回收掉。
get
func (p *Pool) Get() any {
// 把当前goroutine固定在当前的P上
l, pid := p.pin()
// 优先从local的private字段取,快速
x := l.private
l.private = nil
if x == nil {
// 从当前的local.shared弹出一个,注意是从head读取并移除
x, _ = l.shared.popHead()
if x == nil {
// 如果没有,则去偷一个
x = p.getSlow(pid)
}
}
runtime_procUnpin()
// 如果没有获取到,尝试使用New函数生成一个新的
if x == nil && p.New != nil {
x = p.New()
}
return x
}
Get:
- 从本地的 private 字段中获取可用元素,因为没有锁,获取元素的过程会非常快
- 如果没有获取到,就尝试从本地的 shared 获取一个
- 如果还没有,会使用 getSlow 方法去其它的 shared 中“偷”一个。
- 最后,如果没有获取到,就尝试使用 New 函数创建一个新的
func (p *Pool) getSlow(pid int) any {
size := runtime_LoadAcquintptr(&p.localSize) // load-acquire
locals := p.local // load-consume
// 从其它proc中尝试偷取一个元素
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i+1)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// 如果其它proc也没有可用元素,那么尝试从vintim中获取
size = atomic.LoadUintptr(&p.victimSize)
if uintptr(pid) >= size {
return nil
}
locals = p.victim
l := indexLocal(locals, pid)
// 同样的逻辑,先从vintim中的local private获取
if x := l.private; x != nil {
l.private = nil
return x
}
// 从vintim其它proc尝试偷取
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// 如果victim中都没有,则把这个victim标记为空,以后的查找可以快速跳过了
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin()
// 在 pinSlow 函数中我们先将数据存储到 local 内存指针数组,再存储到 localSize 变量中,而这里我们则是相反的顺序进行加载。
// 由于我们已经禁用了抢占,因此垃圾回收器无法在其中发生。
// 因此,在这里我们必须至少观察到 local 大小不小于 localSize。
// 我们可以观察到一个更新/更大的 local,这是可以的(我们必须观察到它被初始化为零)。
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local // load-consume
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
return p.pinSlow()
}
Put
func (p *Pool) Put(x any) {
// nil值直接丢弃
if x == nil {
return
}
l, _ := p.pin()
// 如果本地private没有值,直接设置这个值即可
if l.private == nil {
l.private = x
} else {
// 否则加入到本地队列中
l.shared.pushHead(x)
}
runtime_procUnpin()
}
Put:
- 优先放到本地private
- 如果 private 字段已经有值了,那么就把此元素 push 到本地队列中
clean
func poolCleanup() {
// 丢弃当前victim, STW所以不用加锁
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// 将local复制给victim, 并将原local置为nil
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
cleanup:
- victim清空,local放到victim
- local清空
总结
sync.Pool
内部使用了两个数据结构:local
和victim
。local
是一个指针数组,每个元素都是一个临时对象池,其中存放了多个可重用的临时对象。victim
是一个指针,它指向上一次GC时没有被清理的临时对象,这些对象有可能被其他的sync.Pool
实例重复利用。
sync.Pool
实现了两个方法:Get
和Put
,用于获取和释放临时对象。
当调用Get
方法时,sync.Pool
会首先尝试从当前goroutine对应的local
对象池中获取一个可重用的临时对象,如果获取成功,则直接返回该对象;否则会尝试从victim
指向的对象池中获取一个可重用的临时对象,如果获取成功,则直接返回该对象;最后,如果没有可重用的对象,则根据New
方法的设置,创建一个新的对象返回。
当调用Put
方法时,sync.Pool
会将临时对象放回当前goroutine对应的local
对象池中,如果local
对象池已经满了,则会将其中一些对象放入victim
指向的对象池中,等待下次被重用。
在每次GC之前,sync.Pool
会调用release
方法来释放所有被临时对象池持有的对象,以便这些对象可以被垃圾回收器回收。
sync.Pool
通过使用对象池技术来避免频繁创建和销毁临时对象,从而提高应用程序的性能。其实现原理主要包括了两个数据结构local
和victim
,以及Get
、Put
、release
等方法,通过这些方法实现了临时对象的获取、释放和清理等功