Go Map、Slice线程安全研究以及append内存重复利用

sync.Map存的是值

Slice是引用类型

sync.Pool是引用类型

sync.Map存的是值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var syncMap sync.Map
persons := 4
syncMap.Store(666, persons)
aa, _ := syncMap.Load(666)
fmt.Println(aa)
bb, _ := syncMap.Load(666)
bb = 8
fmt.Println(bb)
fmt.Println(aa)
}
------------
4
8
4

模拟多线程修改从sync.Map中Load的值,修改是隔离的,aa未被修改。对于Struct也是如此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
type person struct{
Height int
}
personA:= person{
169,
}
syncMap.Store(666,personA)
aa, _ := syncMap.Load(666)
fmt.Println(aa.(person).Height)
bb, _ := syncMap.Load(666)
anPerson := bb.(person)
anPerson.Height = 159
fmt.Println(bb.(person).Height)
fmt.Println(aa.(person).Height)
}
------------
169
169
169

Slice是引用类型

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
func main() {
type Person struct{
Height int
}

persons := make([]Person, 2)
persons[0] = Person{
Height: 159,
}
persons[1] = Person{
Height: 169,
}
syncMap.Store(666, persons)
aa, _ := syncMap.Load(666)
fmt.Println((aa.([]Person))[0].Height)
bb, _ := syncMap.Load(666)

(bb.([]Person))[0].Height = 90

fmt.Println((bb.([]Person))[0].Height)
fmt.Println((aa.([]Person))[0].Height)
}
------------
159
90
90

在不同线程中修改从sync.Map中Load的Slice,修改是非隔离的。例如在其他线程修改了bb,aa也会受影响

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
func changePool(pool sync.Pool) {
tmp := pool.Get()
fmt.Println(tmp)
//pool.Put(tmp)
}

func main() {
aPool := sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
},
}
persons := make([]Person, 2)
persons[0] = Person{
Height: 159,
}
persons[1] = Person{
Height: 169,
}

aPool.Put(persons)
changePool(aPool)
newPersons:= aPool.Get()
fmt.Println(newPersons)
}
------------
[{159} {169}]
[{0} {0}]

在changePool中取出元素,在main函数中再次Get,会调用make生成元素

在sync.Pool中存Slice对象还是地址?

我们来测试在一个sync.Pool中存Slice对象地址

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
func TestOnlyCheckIn() {
// 测试仅放回池,不放回sync.Map
type Person struct {
Height int
}
newPool, _ := syncMap.LoadOrStore(2, sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return &slice
},
})
var aPool = newPool.(sync.Pool)
aObj := aPool.Get().(*[]Person)
(*aObj)[0] = Person{
Height: 159,
}
fmt.Println("a初始值:", aObj)
aPool.Put(aObj)
bObj := aPool.Get().(*[]Person)
fmt.Println("b初始值:", bObj)
(*bObj)[0] = Person{
Height: 90,
}
fmt.Println("b修改后值:", bObj)
fmt.Println("a修改后值:", aObj)
}
------------
a初始值: &[{159} {0}]
b初始值: &[{159} {0}]
b修改后值: &[{90} {0}]
a修改后值: &[{90} {0}]

可以看到,假如b线程从sync.Pool里取出地址并修改对象的值,a线程先前存入的对象会被修改。假如存入地址的对象还在使用,就会出现问题。再次放回sync.Map里也是如此

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
func TestCheckIntoMapAndCheckOut() {
// 测试放回池,放回sync.Map后,再依次取出
type Person struct {
Height int
}
newPool, _ := syncMap.LoadOrStore(2, sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return &slice
},
})
var aPool = newPool.(sync.Pool)
aObj := aPool.Get().(*[]Person)
(*aObj)[0] = Person{
Height: 159,
}
fmt.Println("a初始值:", aObj)
aPool.Put(aObj)
syncMap.Store(2, aPool)

newPool2, _ := syncMap.Load(2)
bPool := newPool2.(sync.Pool)
bObj := bPool.Get().(*[]Person)
fmt.Println("b初始值:", bObj)
(*bObj)[0] = Person{
Height: 90,
}
fmt.Println("b修改后值:", bObj)
fmt.Println("a修改后值:", aObj)
}
------------
a初始值: &[{159} {0}]
b初始值: &[{159} {0}]
b修改后值: &[{90} {0}]
a修改后值: &[{90} {0}]

那么sync.Pool直接存Slice对象行不行呢?我们来看一下

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
func TestSafeWayToUseSyncPool() {
// sync.Pool 存对象,而不是存其地址
type Person struct {
Height int
}
newPool, _ := syncMap.LoadOrStore(2, sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
},
})
var aPool = newPool.(sync.Pool)
aObj := aPool.Get().([]Person)
(aObj)[0] = Person{
Height: 159,
}
fmt.Println("a初始值:", aObj)
aPool.Put(aObj)
syncMap.Store(2, aPool)

newPool2, _ := syncMap.Load(2)
bPool := newPool2.(sync.Pool)
bObj := bPool.Get().([]Person)
fmt.Println("b初始值:", bObj)
(bObj)[0] = Person{
Height: 90,
}
fmt.Println("b修改后值:", bObj)
fmt.Println("a修改后值:", aObj)
}
------------
a初始值: &[{159} {0}]
b初始值: &[{159} {0}]
b修改后值: &[{90} {0}]
a修改后值: &[{90} {0}]

并没有和我们想的一样,b的修改不影响a。因为Slice是引用类型,除非是deepCopy,否则对Slice修改,都是修改其内容。这就很危险了,不过要手动回收垃圾,这倒是简便的写法。

确保Slice不会再次使用时,把它放到sync.Pool里,再次取出时直接往Slice里写,这时候是写入老的内存地址中。

有一个隐藏的严重问题。sync.Pool有没有可能是单例?我在sync.Map里存多个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
func TestMutiSyncPool() {
// 测试建立多个互不影响的sync.Pool
type Person struct {
Height int
}

poolWithLen2 := sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
}}
poolWithLen4 := sync.Pool{
New: func() interface{} {
var slice = make([]Person, 4)
return slice
}}
aObj := poolWithLen2.Get().([]Person)
(aObj)[0] = Person{
Height: 159,
}
fmt.Println("a初始值:", aObj)
poolWithLen2.Put(aObj)
bObj := poolWithLen4.Get().([]Person)
fmt.Println("b初始值:", bObj)
aObj = poolWithLen2.Get().([]Person)
fmt.Println("重新取出a打印:", aObj)
}
------------
a初始值: [{159} {0}]
b初始值: [{0} {0} {0} {0}]
重新取出a打印: [{159} {0}]

可喜可贺,sync.Pool支持多个且互不影响。我们可以把不同长度的Person Slice存到一个sync.Map以充分利用内存。

这里其实还忽视了一个隐藏的问题。虽然sync.Pool是引用类型,作为sync.Map值时却不是传引用。下面验证一下

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
func TestSafeWayToUseSyncPoolAndMap() {
// 验证sync.Pool作为sync.Map值时不是传引用
type Person struct {
Height int
}
newPool, _ := syncMap.LoadOrStore(2, sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
},
})
var aPool = newPool.(sync.Pool)
aObj := aPool.Get().([]Person)
(aObj)[0] = Person{
Height: 159,
}
fmt.Println("a初始值:", aObj)
aPool.Put(aObj)
fmt.Println("这里不把aPool存回sync.Map")

newPool2, _ := syncMap.Load(2)
bPool := newPool2.(sync.Pool)
bObj := bPool.Get().([]Person)
fmt.Println("b初始值:", bObj)
(bObj)[0] = Person{
Height: 90,
}
fmt.Println("b修改后值:", bObj)
fmt.Println("a修改后值:", aObj)
}
------------
a初始值: [{159} {0}]
这里不把aPool存回sync.Map
b初始值: [{0} {0}]
b修改后值: [{90} {0}]
a修改后值: [{159} {0}]

悲剧,如果不把Pool存回Map,在外面对Pool的修改是不生效的,如果Pool在Map里存的是地址呢?我们再看一下。

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
func TestNotPutPoolIntoMapButUsePtr() {
// sync.Pool作为sync.Map值时传引用
type Person struct {
Height int
}
newPool, _ := syncMap.LoadOrStore(2, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
},
})
var aPool = newPool.(*sync.Pool)
aObj := aPool.Get().([]Person)
(aObj)[0] = Person{
Height: 159,
}
fmt.Println("a初始值:", aObj)
aPool.Put(aObj)
fmt.Println("这里不把aPool存回sync.Map")

newPool2, _ := syncMap.Load(2)
bPool := newPool2.(*sync.Pool)
bObj := bPool.Get().([]Person)
fmt.Println("b初始值:", bObj)
(bObj)[0] = Person{
Height: 90,
}
fmt.Println("b修改后值:", bObj)
fmt.Println("a修改后值:", aObj)
}
------------
a初始值: [{159} {0}]
这里不把aPool存回sync.Map
b初始值: [{159} {0}]
b修改后值: [{90} {0}]
a修改后值: [{90} {0}]

真棒!sync.Pool可以在各线程中共享值而不必放回sync.Map!

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
type Person struct {
Height int
}
func TestGCinAppend() {
// 测试优化的append
fmt.Println("申请一个长度为2的Slice")
slice2 := []Person{
{234},
{456},
}
fmt.Println("slice2初始值:", slice2)
fmt.Println("append长度不够,申请一个长度为4的Slice,同时把老Slice存入Map")

getPool, _ := syncMap.LoadOrStore(4, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, 4)
return slice
}})
poolWithLen4 := getPool.(*sync.Pool)
newSlice := poolWithLen4.Get().([]Person)
fmt.Println("新的slice2初始值:", newSlice)
copy(newSlice, slice2)
fmt.Printf("#注意这行 把slice2拷到新的slice2后,新的slice2的值:%v,新的slice2长度:%d、容量:%d\n", newSlice, len(newSlice), cap(newSlice))
newSlice = newSlice[:2]
fmt.Printf("Slice截长度之后,新的slice2的值:%v,新的slice2长度:%d、容量:%d\n", newSlice, len(newSlice), cap(newSlice))
fmt.Println("把老的Slice存入Map")
getPool, _ = syncMap.LoadOrStore(2, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
}})
poolWithLen2 := getPool.(*sync.Pool)
poolWithLen2.Put(slice2)

fmt.Println("把新的Slice赋值给slice2")
slice2 = newSlice
fmt.Printf("现在slice2的值:%v,slice2长度:%d、容量:%d\n", slice2, len(slice2), cap(slice2))

fmt.Println("打印poolWithLen2内容")
PrintPool(2)
fmt.Println("打印poolWithLen4内容")
PrintPool(4)

fmt.Println("只有一个元素的slice1 append,用前面申请的内存")
slice1 := []Person{
{888},
}
getPool, _ = syncMap.LoadOrStore(2, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
}})
poolWithLen2 = getPool.(*sync.Pool)
newSlice = poolWithLen2.Get().([]Person)
fmt.Println("新的slice1初始值:", newSlice)
copy(newSlice, slice1)
fmt.Printf("#注意这行 把slice1拷到新的slice1后,新的slice1的值:%v,slice4长度:%d、容量:%d\n", newSlice, len(newSlice), cap(newSlice))
newSlice = newSlice[:1]
fmt.Printf("Slice截长度之后,新的slice1的值:%v,slice4长度:%d、容量:%d\n", newSlice, len(newSlice), cap(newSlice))
fmt.Println("把老的Slice存入Map")
getPool, _ = syncMap.LoadOrStore(1, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, 2)
return slice
}})
poolWithLen1 := getPool.(*sync.Pool)
poolWithLen1.Put(slice1)

fmt.Println("把新的Slice赋值给slice1")
slice1 = newSlice
fmt.Printf("现在slice1的值:%v,slice1长度:%d、容量:%d\n", slice1, len(slice1), cap(slice1))

fmt.Println("打印poolWithLen1内容")
PrintPool(1)
fmt.Println("打印poolWithLen2内容")
PrintPool(2)
fmt.Println("打印poolWithLen4内容")
PrintPool(4)
}
func PrintPool(i int) {
var objCollect [][]Person
getPool, _ := syncMap.Load(i)
aPool := getPool.(*sync.Pool)
for ; ; {
aObj := aPool.Get().([]Person)
if (aObj)[0].Height == 0 {
break
}
fmt.Printf("打印Pool中的元素:%v\n", aObj)
objCollect = append(objCollect, aObj)
}
for _, x := range objCollect {
aPool.Put(x)
}
}
------------
申请一个长度为2的Slice
slice2初始值: [{234} {456}]
append长度不够,申请一个长度为4的Slice,同时把老Slice存入Map
新的slice2初始值: [{0} {0} {0} {0}]
#注意这行 把slice2拷到新的slice2后,新的slice2的值:[{234} {456} {0} {0}],新的slice2长度:4、容量:4
Slice截长度之后,新的slice2的值:[{234} {456}],新的slice2长度:2、容量:4
把老的Slice存入Map
把新的Slice赋值给slice2
现在slice2的值:[{234} {456}],slice2长度:2、容量:4
打印poolWithLen2内容
打印Pool中的元素:[{234} {456}]
打印poolWithLen4内容
只有一个元素的slice1 append,用前面申请的内存

新的slice1初始值: [{234} {456}]
#注意这行 把slice1拷到新的slice1后,新的slice1的值:[{888} {456}],slice4长度:2、容量:2
Slice截长度之后,新的slice1的值:[{888}],slice4长度:1、容量:2
把老的Slice存入Map
把新的Slice赋值给slice1
现在slice1的值:[{888}],slice1长度:1、容量:2
打印poolWithLen1内容
打印Pool中的元素:[{888}]
打印poolWithLen2内容
打印poolWithLen4内容

写对不容易,忽略一堆魔鬼变量取名T T。精简一下,得到最终测试代码。

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
type Person struct {
Height int
}
func ExtraAppend(personSlice *[]Person, newObj Person) {
if personSlice != nil && len(*personSlice) == cap(*personSlice) {
oldLength := len(*personSlice)
getPool, _ := syncMap.LoadOrStore(oldLength*2, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, oldLength*2)
return slice
}})
poolWithLenDouble := getPool.(*sync.Pool)
newSlice := poolWithLenDouble.Get().([]Person)
copy(newSlice, *personSlice)
newSlice = newSlice[:oldLength]
getPool, _ = syncMap.LoadOrStore(oldLength, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, oldLength)
return slice
}})
poolWithLenOrigin := getPool.(*sync.Pool)
poolWithLenOrigin.Put(*personSlice)
*personSlice = newSlice
fmt.Println("扩容完毕")
}
*personSlice = append(*personSlice, newObj)
}
func TestExtraAppend() {
slice2 := []Person{
{234},
{456},
}
ExtraAppend(&slice2, Person{
555,})
fmt.Printf("现在slice2的值:%v,slice2长度:%d、容量:%d\n", slice2, len(slice2), cap(slice2))
slice1 := []Person{
{888},
}
ExtraAppend(&slice1, Person{
8911,})
fmt.Printf("现在slice1的值:%v,slice1长度:%d、容量:%d\n", slice1, len(slice1), cap(slice1))
fmt.Printf("现在slice2的值:%v,slice2长度:%d、容量:%d\n", slice2, len(slice2), cap(slice2))
PrintPool(1)
PrintPool(2)
PrintPool(3)
PrintPool(4)
fmt.Println("执行一次runtime.GC()")
runtime.GC()
fmt.Printf("现在slice2的值:%v,slice2长度:%d、容量:%d\n", slice2, len(slice2), cap(slice2))
fmt.Printf("现在slice1的值:%v,slice1长度:%d、容量:%d\n", slice1, len(slice1), cap(slice1))
PrintPool(1)
PrintPool(2)
PrintPool(3)
PrintPool(4)
}
func PrintPool(i int) {
fmt.Printf("打印Pool中的元素,长度为:%d\n", i)
var objCollect [][]Person
getPool, _ := syncMap.LoadOrStore(i, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, i)
return slice
}})
aPool := getPool.(*sync.Pool)
for ; ; {
aObj := aPool.Get().([]Person)
if (aObj)[0].Height == 0 {
break
}
fmt.Printf("元素:%v,元素长度:%d、容量:%d\n", aObj, len(aObj), cap(aObj))
objCollect = append(objCollect, aObj)
}
for _, x := range objCollect {
aPool.Put(x)
}
}
------------
扩容完毕
现在slice2的值:[{234} {456} {555}],slice2长度:3、容量:4
扩容完毕
现在slice1的值:[{888} {8911}],slice1长度:2、容量:2
现在slice2的值:[{234} {456} {555}],slice2长度:3、容量:4
打印Pool中的元素,长度为:1
元素:[{888}],元素长度:1、容量:1
打印Pool中的元素,长度为:2
打印Pool中的元素,长度为:3
打印Pool中的元素,长度为:4
执行一次runtime.GC()
现在slice2的值:[{234} {456} {555}],slice2长度:3、容量:4
现在slice1的值:[{888} {8911}],slice1长度:2、容量:2
打印Pool中的元素,长度为:1
打印Pool中的元素,长度为:2
打印Pool中的元素,长度为:3
打印Pool中的元素,长度为:4

每次系统GC,会带走所有sync.Pool中的元素。下面是第一版错误的例子23333

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
type Person struct {
Height int
}
func TestExtraAppend() {
slice2 := []Person{
{234},
{456},
}
ExtraAppendFailed(&slice2, Person{
555,})
fmt.Printf("现在slice2的值:%v,slice2长度:%d、容量:%d\n", slice2, len(slice2), cap(slice2))
slice1 := []Person{
{888},
}
ExtraAppendFailed(&slice1, Person{
8911,})

fmt.Printf("现在slice1的值:%v,slice1长度:%d、容量:%d\n", slice1, len(slice1), cap(slice1))
fmt.Printf("现在slice2的值:%v,slice2长度:%d、容量:%d\n", slice2, len(slice2), cap(slice2))
}
func ExtraAppendFailed(personSlice *[]Person, newObj Person) {
if personSlice != nil && len(*personSlice) == cap(*personSlice) {
oldLength := len(*personSlice)
getPool, _ := syncMap.LoadOrStore(oldLength*2, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, oldLength*2)
return &slice
}})
poolWithLenDouble := getPool.(*sync.Pool)
newSlice := poolWithLenDouble.Get().(*[]Person)
copy(*newSlice, *personSlice)
var tmpSlice = *newSlice
*newSlice = tmpSlice[:oldLength]
getPool, _ = syncMap.LoadOrStore(oldLength, &sync.Pool{
New: func() interface{} {
var slice = make([]Person, oldLength)
return &slice
}})
poolWithLenOrigin := getPool.(*sync.Pool)
poolWithLenOrigin.Put(personSlice)
*personSlice = *newSlice
fmt.Println("扩容完毕")
}
*personSlice = append(*personSlice, newObj)
}
------------
扩容完毕
现在slice2的值:[{234} {456} {555}],slice2长度:3、容量:4
扩容完毕
现在slice1的值:[{888} {8911}],slice1长度:2、容量:4
现在slice2的值:[{888}],slice2长度:1、容量:4

Pool里存对象地址,在第一轮append的时候,slice2的内容变成了长度4的Slice,导致slice1拿到的newSlice出错。并且对newSlice修改之后,还会改动slice2的内容。这是非常严重的运行时错误。

参考资料