Go Sync.Pool 背后的想法

概述

我最近在我的一个项目中遇到了垃圾回收问题。大量对象被重复分配,并导致 GC 的巨大工作量。使用 sync.Pool,我能够减少分配和 GC 工作负载。

什么是 sync.Pool?

Go 1.3 版本的亮点之一是同步池。它是 sync 包下的一个组件,用于创建自我管理的临时检索对象池。

为什么要使用 sync.Pool?

我们希望尽可能减少 GC 开销。频繁的内存分配和回收会给 GC 带来沉重的负担。sync.Poll 可以缓存暂时不使用的对象,并在下次需要时直接使用它们(无需重新分配)。这可能会减少 GC 工作负载并提高性能。

怎么使用 sync.Pool?

首先,您需要设置新函数。当池中没有缓存对象时将使用此函数。之后,您只需要使用 GetPut 方法来检索和返回对象。另外,池在第一次使用后绝对不能复制。

由于 New 函数类型是 func() interface{}Get 方法返回一个 interface{}。为了得到具体对象,你需要做一个类型断言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// A dummy struct
type Person struct {
Name string
}

// Initializing pool
var personPool = sync.Pool{
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
New: func() interface{} { return new(Person) },
}

// Main function
func main() {
// Get hold of an instance
newPerson := personPool.Get().(*Person)
// Defer release function
// After that the same instance is
// reusable by another routine
defer personPool.Put(newPerson)

// Using the instance
newPerson.Name = "Jack"
}

基准测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type Person struct {
Age int
}

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

func BenchmarkWithoutPool(b *testing.B) {
var p *Person
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
p = new(Person)
p.Age = 23
}
}
}

func BenchmarkWithPool(b *testing.B) {
var p *Person
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
p = personPool.Get().(*Person)
p.Age = 23
personPool.Put(p)
}
}
}

测试结果:

1
2
3
4
BenchmarkWithoutPool
BenchmarkWithoutPool-8 160698 ns/op 80001 B/op 10000 allocs/op
BenchmarkWithPool
BenchmarkWithPool-8 191163 ns/op 0 B/op 0 allocs/op

权衡

生活中的一切都是一种权衡。池也有它的性能成本。使用 sync.Pool 比简单的初始化要慢得多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func BenchmarkPool(b *testing.B) {
var p sync.Pool
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
p.Put(1)
p.Get()
}
})
}

func BenchmarkAllocation(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := 0
i = i
}
})
}

压测结果:

1
2
3
4
BenchmarkPool
BenchmarkPool-8 283395016 4.40 ns/op
BenchmarkAllocation
BenchmarkAllocation-8 1000000000 0.344 ns/op

sync.Pool 是如何工作的?

sync.Pool 有两个对象容器: 本地池 (活动) 和受害者缓存 (存档)。

根据 sync/pool.go ,包 init 函数作为清理池的方法注册到运行时。此方法将由 GC 触发。

1
2
3
func init() {
runtime_registerPoolCleanup(poolCleanup)
}

当 GC 被触发时,受害者缓存中的对象将被收集,然后本地池中的对象将被移动到受害者缓存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}

新对象被放入本地池中。调用 Put 方法也会将对象放入本地池中。调用 Get 方法将首先从受害者缓存中获取对象,如果受害者缓存为空,则对象将从本地池中获取。
sync.Pool localPool and victimCache

供你参考,Go 1.12 sync.pool 实现使用基于 mutex 的锁,用于来自多个 Goroutines 的线程安全操作。Go 1.13 引入了一个双链表作为共享池,它删除了 mutex 并改善了共享访问。

结论

当有一个昂贵的对象需要频繁创建时,使用 sync.Pool 是非常有益的。

译自:https://medium.com/swlh/go-the-idea-behind-sync-pool-32da5089df72

微信订阅号

-------------本文结束感谢您的阅读-------------