目录
- 一. sync.Pool 基础
-
- 使用示例
- 性能测试
-
- 1. struct 反序列化
- 2. bytes.Buffer
- 二. sync.Pool 在标准库中的应用
- 三. 源码分析
-
-
- 1. Put()添加方法
- 2. Get()获取
- 3. sync.Pool的GC垃圾回收
- 4. 小总结
一. sync.Pool 基础
- 是Go1.3发布的一个特性,当项目中频繁的创建回收对象时,造成了GC的压力不必要的资源浪费,从而考虑使用sync.pool缓存对象,当需要是通过这个缓冲池获取
- sync.Pool 是可伸缩的,并发安全的,高负载时会动态扩容,大小受限于内存大小,存放在池中对象不活跃会被自动清理
- sync.Pool 使用时先通过New设置返回数据函数,该函数决定Pool中存储的什么数据,提供了Get, Put
- Get() 用于从对象池中获取对象,因为返回值时 interface{} 因此需要值类型转换
- Put() 则是在对象使用完之后放回对象池
使用示例
- 声明对象池
type Student struct {
Name string
Age int32
Remark [1024]byte
}
var studentPool = sync.Pool{
New: func() interface{} {
return new(Student)
},
}
- Get() 用于从对象池中获取对象,因为返回值是 interface{},因此需要类型转换。
- Put() 则是在对象使用完毕后,返回对象池
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
性能测试
1. struct 反序列化
- 因为 Student 结构体内存占用较小,内存分配几乎不耗时间。而如果使用标准库 json 反序列化,内部利用了反射,效率是比较低的,占据了大部分时间,因此两种方式最终的执行时间几乎没什么变化。但是内存占用差了一个数量级,使用了 sync.Pool 后,内存占用仅为未使用的 234/5096 = 1/22,对 GC 的影响就很大了
func BenchmarkUnmarshal(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := &Student{}
json.Unmarshal(buf, stu)
}
}
func BenchmarkUnmarshalWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
}
}
2. bytes.Buffer
- 创建了一个 bytes.Buffer 对象池,而且每次只执行一个简单的 Write 操作,存粹的内存搬运工,耗时几乎可以忽略。而内存分配和回收的耗时占比较多,因此对程序整体的性能影响更大
var bufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
var data = make([]byte, 10000)
func BenchmarkBufferWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Write(data)
buf.Reset()
bufferPool.Put(buf)
}
}
func BenchmarkBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
var buf bytes.Buffer
buf.Write(data)
}
}
二. sync.Pool 在标准库中的应用
- 例如fmt.Printf 的调用是非常频繁的,利用 sync.Pool 复用 pp 对象能够极大地提升性能,减少内存占用,同时降低 GC 压力
type pp struct {
buf buffer
...
}
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
func (p *pp) free() {
if cap(p.buf) > 64<<10 {
return
}
p.buf = p.buf[:0]
p.arg = nil
p.value = reflect.Value{}
p.wrappedErr = nil
ppFree.Put(p)
}
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
三. 源码分析
- 了解sync.Pool得先 了解golang的调度模型MPG
- M:Machine,一个M直接关联了一个内核线程, 一个groutine最终是要放到M上执行的;
- P:processor,存储当前go协程运行的上下文环境(函数指针,堆栈地址及地址边界),会对自己的go协程队列做一些调度(把占用CPU时间较长的go协程暂停,运行后续的goroutine)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务;
- G:指的是Goroutine,其实本质上也是一种轻量级的线程,里面除了存放本goroutine信息外 还有与所在P的绑定等信息
- 先看一下sync.Pool结构体,在Pool结构体中提供了一个local 字段,该字段实际是[P]poolLocal类型,数据实际就是存储在这个字段上的,其中的P就是MPG模型中的P,sync.Pool为每个P(对应CPU)都分配一个本地池,当执行Get或者Put操作时,会先将goroutine和某个P的子池关联,再对该子池进行操作
type Pool struct {
noCopy noCopy
local unsafe.Pointer
localSize uintptr
victim unsafe.Pointer
victimSize uintptr
New func() interface{}
}
- sync.Pool结构体中提供的local类型实际是poolLocal,是对poolLocalInternal的封装,结合起来看一下这两个结构体
- 在poolLocalInternal中存在private私有对象属性与shared 共享对象列表属性,私有对象只能被特定的P访问,共享列表对象可以被任何P访问,在操作私有对象属性时因为同一时刻一个P只能执行一个goroutine,所以无需加锁,但是对共享列表对象进行操作时,因为可能有多个goroutine同时操作,所以需要加锁
- poolLocal 是对 poolLocalInternal 的封装,内部又提供了一个pad字段,目的是为了防止false sharing。cache使用中常见的一个问题是false sharing。当不同的线程同时读写同一cache line上不同数据时就可能发生false sharing。false sharing会导致多核处理器上严重的系统性能下降
type poolLocalInternal struct {
private interface{}
shared poolChain
}
type poolLocal struct {
poolLocalInternal
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
1. Put()添加方法
- 当创建Pool实例后需要调用Put()方法将数据存入池中,该方法中
- 先对数据进行校验判断是否非空,判断race.Enabled开关
- 调用p.pin()获取存储数据的poolLocal,并将当前goroutine绑定到P上,防止当前的p被其他的g抢占
- 判断poolLocal的private属性是否设置过私有值,如果没有直接将数据添加到poolLocal的private
- 如果poolLocal的private不为null说明设置了pool的私有值,执行l.shared.pushHead(x),将数据追加到poolLocal的shared共享列表的顶部
- 数据添加完毕执行runtime_procUnpin()取消 P 的不可抢占状态
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
if race.Enabled {
if fastrand()%4 == 0 {
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
l, _ := p.pin()
if l.private == nil {
l.private = x
x = nil
}
if x != nil {
l.shared.pushHead(x)
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
- 内部的p.pin(): 将当前goroutine固定到 调度器P,禁用抢占并返回调度器P和P的id对应的poolLocal pool
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin()
s := atomic.LoadUintptr(&p.localSize)
l := p.local
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
return p.pinSlow()
}
- 内部p.pinSlow() 该方法主要进行全局加锁,重新分配数组内存,添加到全局列表
func (p *Pool) pinSlow() (*poolLocal, int) {
runtime_procUnpin()
allPoolsMu.Lock()
defer allPoolsMu.Unlock()
pid := runtime_procPin()
s := p.localSize
l := p.local
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
if p.local == nil {
allPools = append(allPools, p)
}
size := runtime.GOMAXPROCS(0)
local := make([]poolLocal, size)
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0]))
atomic.StoreUintptr(&p.localSize, uintptr(size))
return &local[pid], pid
}
var (
allPoolsMu Mutex
allPools []*Pool
oldPools []*Pool
)
2. Get()获取
- 该方法主要实现在池中选择任意项,将其从池中移除,并将其返回给调用者
func (p *Pool) Get() interface{} {
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 race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
if x == nil && p.New != nil {
x = p.New()
}
return x
}
- 内部的p.getSlow(pid), 就是偷steal,当前localPool中没有缓存的对象时就不能从 private 或 shared获取、为了提高性能可以 从别的P那里偷一个这样比创建一个新的要快
func (p *Pool) getSlow(pid int) interface{} {
size := atomic.LoadUintptr(&p.localSize)
locals := p.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 = 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
}
}
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
3. sync.Pool的GC垃圾回收
- 看一下sync.pool.go这个文件,该文件中提供了一个init()函数,该函数中调用了registerPoolCleanup()就是垃圾回收相关的
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
func runtime_registerPoolCleanup(cleanup func())
- 继续向下追,在registerPoolCleanup()中调用了poolCleanup(),实际就是 将所有的对象置为 nil,等着GC做自动回收
func poolCleanup() {
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
4. 小总结
- sync.Pool 的设计初衷是对象的复用,避免重复创建、销毁。将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力
- sync.Pool的应用场景:第一是当我们必须重用 共享和长期存在的对象时,第二是用于优化内存分配。使用顺序是 设置好 New 函数后,调用 Get 获取,调用 Put 归还对象
- 考虑清楚复用对象的生命周期,在使用时,不要对 Get 得到的对象有任何假设,默认Get到对象是一个空对象,Get之后手动初始化。
- 好的实践是:Put操作执行前将对象“清空”,并且确保对象被Put进去之后不要有任何的指针引用再次使用,不然极大概率导致data race。
- Pool 里对象的生命周期受 GC 影响,不适合于做连接池,因为连接池需要自己管理对象的生命周期
- 在加入 victim 机制前,sync.Pool 里对象的最⼤缓存时间是一个 GC 周期,当 GC 开始时,没有被引⽤的对象都会被清理掉;加入 victim 机制后,最大缓存时间为两个 GC 周期