Go版本1.13.1
Go中有sync.Pool类型,我们可以把它理解成存放临时值的容器,之所以加上“临时”两个字,是因为它会在GC过程的STW步骤被清理。
sync.Pool类型使用前可以给它的New字段赋值,New字段类型是func() interface{},一个函数类型,该函数一般在池内为空的时候才会调用
sync.Pool有两个公开的方法,一个Put,一个Get,作用看函数名就知道了
Go的并发模型是GMP模型,sync.Pool给每个P都建立了本地池,一个本地私有池,一个本地共享池,执行Get方法时,先从本地私有池取,取不到,去本地共享池,再取不到,去其他P的共享池中取,失败的话去victim cache中取,再失败就调用New方法,New生成的对象不会放到本地池中,是直接返回给调用方的。
我今天在书上发现一个“坑”,其实是go版本的问题导致的,书中的例子大概是这样的
package main
import (
"fmt"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
)
var (
count int32
initFunc = func() interface{} {
return atomic.AddInt32(&count, 1)
}
pool = sync.Pool{New:initFunc}
)
func main() {
debug.SetGCPercent(debug.SetGCPercent(-1))
v1 := pool.Get()
fmt.Printf("value 1: %v\n", v1)
pool.Put(10)
pool.Put(11)
pool.Put(12)
v2 := pool.Get()
fmt.Printf("value 2: %v\n", v2)
runtime.GC()
v3 := pool.Get()
fmt.Printf("value 3: %v\n", v3)
pool.New = nil
v4 := pool.Get()
fmt.Printf("value 4: %v\n", v4)
}
// 书中的输出结果
value 1: 1
value 2: 10
value 3: 2
value 4: <nil>
// 我实际的输出结果
value 1: 1
value 2: 10
value 3: 11
value 4: 12
例子里是想展示GC时会清空Pool里面的元素,清空后会调用New方法
我实际执行发现结果和书上的不一样,我当时在想怎么会出现这种情况,难道GC有问题?后来经过翻看源码,查看GC日志,发现一个关键的代码段,sync.Pool在STW时会执行poolCleanup函数
func poolCleanup() {
// Drop victim caches from all pools.
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// Move primary cache to victim cache.
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
问题就出现在上面这个函数中,我这个版本的go,sync.Pool在GC时,数据会转到victim里面,也就是说会幸存一次GC,所以要实现书中的效果,需要两次GC。
后面去查看go代码提交日志,确实发现了这个代码的提交记录
总结
总结一下sync.Pool的两个特性
- 对垃圾回收友好
- 可以把对象值产生的存储压力进行分摊
有疑问加站长微信联系(非本文作者)