介绍

一、

临时对象池(pool)的设计目的是用来保存和复用临时对象,以减少内存分配,降低CG压力; 同时, 利用对象还实现了临时对象的复用, 从而降低某些场景下重复申请内存所消耗的时间。

想感受下对象池的威力的话, 就一起看一个简单的例子吧:

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 main

import (
"fmt"
"sync"
"time"
)

type structR6 struct {
B1 [100000]int
}

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

func usePool() {
startTime := time.Now()
for i := 0; i < 10000; i++ {
sr6 := r6Pool.Get().(*structR6)
sr6.B1[0] = 0
sr6.B1[1] = 5
r6Pool.Put(sr6) // 放回池中,防止下个循环,Get时重新生成(防止因自动Gc造成的cpu时间片与系统资源的浪费)
}
fmt.Println("pool Used:", time.Since(startTime).Microseconds())
}

func standard() {
startTime := time.Now()
for i := 0; i < 10000; i++ {
var sr6 *structR6 = new(structR6)
sr6.B1[0] = 0
sr6.B1[1] = 5
}
fmt.Println("standard Used:", time.Since(startTime).Microseconds())
}
func main() {
standard()
usePool()
}

执行结果如下图:

一个含有100000个int值的结构体,在标准方法中,每次均新建,重复10000次,一共需要耗费2449615us

如果用完的struct可以废物利用,放回pool中。需要新的结构体的时候,尝试去pool中取,而不是重新生成,重复10000次需要的时间几乎可以忽略(可以增加本案例中 自定义结构体成员int数组的大小, 来做更细致的验证)。

也就是说, 大大节省了 GC 和 新建 对象的 资源消耗。 但是对于一个占用内存本就不是很大的对象/结构体来说, 复用的意义就不是很大了, 这时使用sync.Pool甚至可能增加资源消耗。

您可以通过对 结构体中 int数组 B1 的大小做调整, 从而进一步验证此现象。 或者 移步 这篇文章-> 对于大批量文件的解析操作优化_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
package main

import (
"fmt"
"sync"
"time"
)

type structR6 struct {
B1 [100000]int
}

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

func usePool() {
startTime := time.Now()
cont := 0
for i := 0; i < 10000; i++ {
sr6 := r6Pool.Get().(*structR6)
sr6.B1[0] = 0
sr6.B1[1] = 5
r6Pool.Put(sr6) // 放回池中,防止下个循环,Get时重新生成(防止因自动Gc造成的cpu时间片与系统资源的浪费)
}
// 为了验证, put仅池子里的结构体, 有没有被置为 0结构体 -> 比如, 数组 中的 sr6.B1[1] = 5 会重新被置0, 相当于回收站, 只不过暂时没有清理其所占的实际内存而已
for i := 0; i < 10000; i++ {
sr6 := r6Pool.Get().(*structR6)
sr6.B1[0] = 1
if(sr6.B1[1] == 5){
cont += 1
}
r6Pool.Put(sr6) // 放回池中,防止下个循环,Get时重新生成(防止因自动Gc造成的cpu时间片与系统资源的浪费)
}
fmt.Printf("pool cont = %d 次\n", cont)
fmt.Println("pool Used:", time.Since(startTime).Microseconds())
}

func standard() {
startTime := time.Now()
cont := 0
for i := 0; i < 10000; i++ {
var sr6 *structR6 = new(structR6)
sr6.B1[0] = 0
sr6.B1[1] = 5
}
for i := 0; i < 10000; i++ {
var sr6 *structR6 = new(structR6)
sr6.B1[0] = 1
if(sr6.B1[1] == 5){
cont += 1
}
}
fmt.Printf("standard cont = %d 次\n", cont)
fmt.Println("standard Used:", time.Since(startTime).Microseconds())
}

func main() {
standard()
usePool()
}

执行结果如下图:

我们可以看到, 被加入池中复用的数组, 被取出来时, 还是有可能和原来一个样子的, 并没有对值做重置操作。 因此, 如果您的对象, 在业务上并不是能够复用的, 那么最好在放入sync.Pool前做些什么, 不然不建议使用。

对于对象中的切片, 如果您想要复用其底层数组的话, 需要使用 [:0]来对其进行重置, 当然如果您想放弃底层数组的复用, 让它被GC干掉, 你可以使用 nil来重置数组。

三、

如果您的复用对象非常重要, 建议你手动管理此对象的创建和释放时机, 而不是来选择sync.Pool。 因为, sync.Pool 中对象的回收是依赖与 GC 的, 最多也就比其他对象多活两轮GC, 所以很可能被定期的GC给干掉。

基于之前的例子, 我们几乎都是在复用最初创建的第一个 new(structR6), 也就是只开辟了一次空间, 然后一直复用。 由于时间很短, 在进入验证循环前, 有可能并没有遇到过GC, 或者只遇到过1次GC。

因此, 我们 在进入验证循环前, 进行手动 GC , 来模拟出 sync.Poll 内对象的自动回收的必要条件。

下面是测试代码:

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
package main

import (
"runtime"
"fmt"
"sync"
"time"
)

type structR6 struct {
B1 [100000]int
}

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

func usePool() {
startTime := time.Now()
cont := 0
for i := 0; i < 10000; i++ {
sr6 := r6Pool.Get().(*structR6)
sr6.B1[0] = 0
sr6.B1[1] = 5
r6Pool.Put(sr6) // 放回池中,防止下个循环,Get时重新生成(防止因自动Gc造成的cpu时间片与系统资源的浪费)
}
// 在此处进行手动 GC。 可以发现, 当只执行一次时, 是有可能还会出现 10000 的情况的; 但若手动执行两次, 则对象被回收掉就是板上钉钉的事了(因为源码中的缓存机制就是最多两次GC的周期)
runtime.GC()
// 为了验证, put仅池子里的结构体, 有没有被置为 0结构体 -> 比如, 数组 中的 sr6.B1[1] = 5 会重新被置0, 相当于回收站, 只不过暂时没有清理其所占的实际内存而已
for i := 0; i < 10000; i++ {
sr6 := r6Pool.Get().(*structR6)
sr6.B1[0] = 1
if(sr6.B1[1] == 5){
cont += 1
}
r6Pool.Put(sr6) // 放回池中,防止下个循环,Get时重新生成(防止因自动Gc造成的cpu时间片与系统资源的浪费)
}
fmt.Printf("pool cont = %d 次\n", cont)
fmt.Println("pool Used:", time.Since(startTime).Microseconds())
}

func main() {
usePool()
}

执行结果:

  • 单次GC (可以看到执行结果不固定, 因为 sync.Pool 中的对象, 实际上最多是可以存活两个GC周期的, 文章后续的源码剖析中会指出):

  • 两次GC:

可以发现, 当只手动执行一次GC时, 是有可能还会出现 10000 的情况的; 但若手动执行两次GC, 则对象被回收掉就是板上钉钉的事了(因为源码中的缓存机制就是最多两次GC的周期)

剖析

源码剖析, 之后有空了再写。