Go Sync.Pool作用及遇到的坑

1,137 阅读2分钟

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的两个特性

  1. 对垃圾回收友好
  2. 可以把对象值产生的存储压力进行分摊