tyltr技术窝

sync.Pool对golang提升性能的作用非常大。而且应用也非常广泛,例如echo框架、gin框架等。

sync.Pool的思想#

因为GC会定期执行,如果代码对某些数据结构,在用到的时候分配内存,不用的时候释放内存,再次用到有分配。。。。这会导致收集器频繁工作,那么大量的
工作都会被分配内存资源了。

那么能不能把这些临时变量进行复用,减少分配和回收的压力。所以产生了sync.Pool

基准测试#

理论是有实验支撑的。那么我们进行基准测试。看看使用sync.Pool与否的性能对比。

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
33
34
35
36
37
38
39
40
41
42

package helloworld

import (
"sync"
"testing"
)

type Hello struct {
a int
}

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


func say(self *Hello) { self.a++ }

func BenchmarkWithoutPool(b *testing.B) {
var s *Hello
for i := 0; i < b.N; i++ {
s = &Hello{a: 1,}
b.StopTimer();
say(s);
b.StartTimer()

}
}

func BenchmarkWithPool(b *testing.B) {
var s *Hello
for i := 0; i < b.N; i++ {
s = pool.Get().(*Hello)
s.a = 1
b.StopTimer();
say(s);
b.StartTimer()
pool.Put(s)

}
}

基准测试的结果如下:

1
2
3

BenchmarkWithoutPool-4 3952010 334 ns/op
BenchmarkWithPool-4 7787623 148 ns/op

都是创建了4个goroutine进行基准测试

  • BenchmarkWithoutPool 执行了3952010次,每次执行的平均时间 334 ns
  • BenchmarkWithPool 执行了7787623次,每次执行的平均时间 148 ns

使用#

sync.Pool是用来暂存临时变量的,并且是goroutine安全的,主要它是重用内存而非重新分配。

sync.Pool只暴露了两个API:Get、 Put 分别用于获取和保存。
创建sync.Pool对象时,New的作用就是如果get不到,就会调用此函数进行创建,起到了默认值的作用。

举例说明: 将byte数组转换成int

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"bytes"
"encoding/binary"
"fmt"
"sync"
"time"
)

// 在实际开发中,sync.Pool对象是共享的
var (
pool = sync.Pool{

// 如果get不到,就会调用此函数进行创建
New: func() interface{} {
return new(bytes.Buffer)
},
}
)

// 将byte数组转int32
func calc(b []byte) (x int32) {
var (
buf *bytes.Buffer
)
// 获取buf
buf = pool.Get().(*bytes.Buffer)
// 向buf写入数据
buf.Write(b)

binary.Read(buf, binary.BigEndian, &x)
fmt.Println(x)

// buf使用完成,置空后放回sync.Pool对象中
buf.Reset()
pool.Put(buf)

return

}

func main() {

bList := [][]byte{{0, 0, 1, 10}, {0, 1, 1, 10}, {1, 0, 1, 10}, {0, 0, 1, 9}, {0, 11, 1, 10}}
for _, value := range bList {
go calc(value)

}
time.Sleep(1 * time.Second)

}

问题:既然Pool是存储临时变量的,随着时间推移,Pool占用的内存会不会一直增长呀,真如此的话,Pool最终也会影响性能的。

如果你能思考到这,我觉得很棒!

其实不必担心Pool会一直增长的问题,因为Go已经帮你在Pool中做了回收机制。请注意上面表述中的措辞sync.Pool是用来暂存临时变量的
保存在Pool中的对象会在没有任何通知的情况下被自动移除掉。也就是说你可能Put进去了10个对象,下次Get的时候发现一个都没有了。
这也可以解释为什么有个New创建默认值。

1
2
3
4
5
6
7
pool = sync.Pool{

// 如果get不到,New 就会调用此函数进行创建
New: func() interface{} {
return new(bytes.Buffer)
},
}

源码解析#

对sync.Pool定时清理#

每次垃圾回收之前进行清理。频率是2分钟一次。源码分析如下:

在sync包内,存在一个全局变量allPools []*Pool 用于存放所有的Pool对象。
函数poolCleanup()用于清空Pool,它会遍历allPools切片,清空Pool内存储对象的。
函数poolCleanup()会在init的时候,注册到runtime,
并且在每次垃圾回收之前进行调用。而且在清空Pool之时,也会产生STW

sync/pool.go

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//  清空pool
func poolCleanup() {
// This function is called with the world stopped, at the beginning of a garbage collection.
// It must not allocate and probably should not call any runtime functions.

// Because the world is stopped, no pool user can be in a
// pinned section (in effect, this has all Ps pinned).

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

// The pools with non-empty primary caches now have non-empty
// victim caches and no pools have primary caches.
oldPools, allPools = allPools, nil
}



var (
allPoolsMu Mutex

// allPools is the set of pools that have non-empty primary
// caches. Protected by either 1) allPoolsMu and pinning or 2)
// STW.
allPools []*Pool

// oldPools is the set of pools that may have non-empty victim
// caches. Protected by STW.
oldPools []*Pool
)



func init() {
// 注册到runtime
runtime_registerPoolCleanup(poolCleanup)
}


// Implemented in runtime.
// 这个注册函数在runtime中实现。
func runtime_registerPoolCleanup(cleanup func())

上面sync包中,已经将清空Pool的函数注册到runtime之中了,runtime中有关清空逻辑如下:
上文知道func runtime_registerPoolCleanup(cleanup func())是在runtime/mgc.go实现的,即sync_runtime_registerPoolCleanup(f func())

在GC之前,会调用gcStart函数,此函数又调用clearpools,clearpools会调用在sync包中注册进来的cleanup,最终实现清空。

runtime/mgc.go

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
var poolcleanup func()

// 此函数就是sync.runtime_registerPoolCleanup的函数的实现
// 将sync.poolCleanup函数注册到此处,使用变量poolcleanup 承接
func sync_runtime_registerPoolCleanup(f func()) {
poolcleanup = f
}


//
func clearpools() {
// clear sync.Pools
// 清空 sync.Pools
if poolcleanup != nil {
poolcleanup()
}

// Clear central sudog cache.
// Leave per-P caches alone, they have strictly bounded size.
// Disconnect cached list before dropping it on the floor,
// so that a dangling ref to one entry does not pin all of them.
lock(&sched.sudoglock)
var sg, sgnext *sudog
for sg = sched.sudogcache; sg != nil; sg = sgnext {
sgnext = sg.next
sg.next = nil
}
sched.sudogcache = nil
unlock(&sched.sudoglock)

// Clear central defer pools.
// Leave per-P pools alone, they have strictly bounded size.
lock(&sched.deferlock)
for i := range sched.deferpool {
// disconnect cached list before dropping it on the floor,
// so that a dangling ref to one entry does not pin all of them.
var d, dlink *_defer
for d = sched.deferpool[i]; d != nil; d = dlink {
dlink = d.link
d.link = nil
}
sched.deferpool[i] = nil
}
unlock(&sched.deferlock)
}



// gcStart starts the GC. It transitions from _GCoff to _GCmark (if
// debug.gcstoptheworld == 0) or performs all of GC (if
// debug.gcstoptheworld != 0).
//
// This may return without performing this transition in some cases,
// such as when called on a system stack or with locks held.
func gcStart(trigger gcTrigger) {


......
// 调用clearpools
clearpools()

.......
}