sync.pool使用介绍


1. 简介

作用:频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺,而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。

为了使得在多个goroutine中高效的使用goroutine,sync.Pool为每个P(对应CPU)都分配一个本地池,

当执行Get或者Put操作的时候,会先将goroutine和某个P的子池关联,再对该子池进行操作。

每个P的子池分为私有对象和共享列表对象,私有对象只能被特定的P访问,共享列表对象可以被任何P访问。

因为同一时刻一个P只能执行一个goroutine,所以无需加锁,但是对共享列表对象进行操作时,

因为可能有多个goroutine同时操作,所以需要加锁。

Put方法:

如果放入的值为空,直接return.

检查当前goroutine的是否设置对象池私有值,如果没有则将x赋值给其私有成员,并将x设置为nil。

如果当前goroutine私有值已经被设置,那么将该值追加到共享列表。

Get方法:

尝试从本地P对应的那个本地池中获取一个对象值, 并从本地池冲删除该值。

如果获取失败,那么从共享池中获取, 并从共享队列中删除该值。

如果获取失败,那么从其他P的共享池中偷一个过来,并删除共享池中的该值(p.getSlow())。

如果仍然失败,那么直接通过New()分配一个返回值,注意这个分配的值不会被放入池中。

New()返回用户注册的New函数的值,如果用户未注册New,那么返回nil。

func init() {

runtime_registerPoolCleanup(poolCleanup)

}

可以看到在init的时候会向GC注册一个PoolCleanup函数,他会清除掉sync.Pool中的所有的缓存的对象,

这个注册函数会在每次GC的时候运行,所以sync.Pool中的值只在两次GC中间的时段有效。

通过以上的解读,我们可以看到,Get方法并不会对获取到的对象值做任何的保证,

因为放入本地池中的值有可能会在任何时候被删除,但是不通知调用者。放入共享池中的值有可能被其他的goroutine偷走。

所以对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,

因为存入对象池重的值有可能会在垃圾回收时被删除掉,这违反了数据库连接池建立的初衷。

根据上面的说法,Golang的对象池严格意义上来说是一个临时的对象池,适用于储存一些会在goroutine间分享的临时对象。主要作用是减少GC,提高性能。

其实sync.pool有点像python线程间共享变量的实现,不同线程间共享变量来实现对象的复用。

2. 使用场景

举个简单的例子:

type Student struct {
	Name   string
	Age    int32
	Remark [1024]byte
}

var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25})

func unmarsh() {
	stu := &Student{}
	json.Unmarshal(buf, stu)
}

json 的反序列化在文本解析和网络通信过程中非常常见,当程序并发度非常高的情况下,短时间内需要创建大量的临时对象。而这些对象是都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。

3. 如何使用

sync.Pool 的使用方式非常简单:

3.1 声明对象池

只需要实现 New 函数即可。对象池中没有对象时,将会调用 New 函数创建。

var studentPool = sync.Pool{
    New: func() interface{} { 
        return new(Student) 
    },
}

3.2 Get & Put

stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
  • Get()用于从对象池中获取对象,因为返回值是interface{}`,因此需要类型转换。
  • Put()` 则是在对象使用完毕后,返回对象池。

4. 性能测试

4.1 struct 反序列化

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)
	}
}

var stu = new(Student)

func BenchmarkUnmarshalWithGlobal(b *testing.B) {
	for n := 0; n < b.N; n++ {
		json.Unmarshal(buf, stu)
	}
}

测试结果如下:

$ go test -bench="^BenchmarkUnmar"  -benchmem
goos: darwin
goarch: amd64
pkg: example/hpg-sync-pool
BenchmarkUnmarshal-8            9277     131042 ns/op      1384 B/op        7 allocs/op
BenchmarkUnmarshalWithPool-8    9441     130150 ns/op       232 B/op        6 allocs/op
BenchmarkUnmarshalWithGlobal-8  9860     136932 ns/op       232 B/op        6 allocs/op
PASS
ok      example/hpg-sync-pool   2.334s

在这个例子中,因为 Student 结构体内存占用较小,内存分配几乎不耗时间。而标准库 json 反序列化时利用了反射,效率是比较低的,占据了大部分时间,因此两种方式最终的执行时间几乎没什么变化。但是内存占用差了一个数量级,第一种没有使用池,每执行一次用例都比后两者多一次内存分配,而第二个使用了 sync.Pool 后,内存占用仅为未使用的 234/5096 = 1/22,对 GC 的影响就很大了。第三个因为引入了全局变量,也不会产生内存分配,故而和第二个用例消耗性能差不多

4.2 bytes.Buffer

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)
	}
}

测试结果如下:

$ go test -bench="^BenchmarkBuffer" -run=none  -benchmem
goos: linux
goarch: amd64
pkg: github.com/go-test/benckmark

BenchmarkBufferWithPool-8      8313493       141 ns/op       0 B/op        0 allocs/op
BenchmarkBuffer-8               795733      1321 ns/op   10240 B/op        1 allocs/op
PASS
ok      github.com/go-test/benckmark    3.408s

这个例子创建了一个 bytes.Buffer 对象池,而且每次只执行一个简单的 Write 操作,存粹的内存搬运工,耗时几乎可以忽略。而内存分配和回收的耗时占比较多,因此对程序整体的性能影响更大。

5. 在标准库中的应用

5.1 fmt.Printf

Go 语言标准库也大量使用了 sync.Pool,例如 fmt 和 encoding/json。

以下是 fmt.Printf 的源代码(go/src/fmt/print.go):

// go 1.13.6

// pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
type pp struct {
    buf buffer
    ...
}

var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.wrapErrs = false
	p.fmt.init(&p.buf)
	return p
}

// free saves used pp structs in ppFree; avoids an allocation per invocation.
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
}

// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

fmt.Printf 的调用是非常频繁的,利用 sync.Pool 复用 pp 对象能够极大地提升性能,减少内存占用,同时降低 GC 压力。

这个例子来源于:深度解密 Go 语言之 sync.Pool


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM