通过引用传递的范围循环变量给Go协程会导致内存泄漏。

huangapple go评论99阅读模式
英文:

For range loop variable passing by reference to go routine causes memory leak

问题

用于复现问题的代码

go版本 go1.19 darwin/amd64

package main

import (
	"bytes"
	"log"
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"strconv"
	"sync"
)

type TestStruct struct {
	M []byte
}

var lkMap sync.Mutex
var tMap map[string]*TestStruct

const objectCount = 1024 * 100

func main() {

	tMap = make(map[string]*TestStruct)

	wg := &sync.WaitGroup{}
	wg.Add(objectCount)
	func1(wg)
	wg.Add(objectCount)
	func2(wg)

	wg.Wait()
	runtime.GC()
	log.Println(http.ListenAndServe("localhost:6060", nil))
}

//go:noinline
func func1(wg *sync.WaitGroup) {
	ts := []*TestStruct{}
	for i := 0; i < objectCount; i++ {
		t := &TestStruct{}
		ts = append(ts, t)
		lkMap.Lock()
		tMap[strconv.Itoa(i)] = t
		lkMap.Unlock()
	}

	for i, t := range ts {
		go func(t *TestStruct, idx int) {
			t.M = bytes.Repeat([]byte{byte(32)}, 1024)

			lkMap.Lock()
			delete(tMap, strconv.Itoa(idx))
			lkMap.Unlock()
			wg.Done()
		}(t, i) //pass by reference
	}
}

//go:noinline
func func2(wg *sync.WaitGroup) {
	ts := []*TestStruct{}
	for i := 0; i < objectCount; i++ {
		t := &TestStruct{}
		ts = append(ts, t)

		lkMap.Lock()
		tMap[strconv.Itoa(i+objectCount)] = t
		lkMap.Unlock()
	}
	for i, t := range ts {
		tmp := t //capture here
		idx := i
		go func() {
			tmp.M = bytes.Repeat([]byte{byte(32)}, 1024)

			lkMap.Lock()
			delete(tMap, strconv.Itoa(idx+objectCount))
			lkMap.Unlock()
			wg.Done()
		}()
	}
}


运行此程序并使用命令行使用pprof获取堆信息:

go tool pprof http://localhost:6060/debug/pprof/heap

将生成如下图所示的图表:
通过引用传递的范围循环变量给Go协程会导致内存泄漏。

从图中可以看出,func1明显存在内存泄漏。func1和func2之间唯一的区别是func2使用一个局部变量来捕获循环变量。

然而,如果我移除全局map,两个函数都没有问题,都没有泄漏。

所以全局map的操作是否有关?

感谢任何意见

编辑:
感谢@nipuna指出在main函数中更改func1和func2的顺序将使func2显示在报告中。然而,func1仍然占用了大部分内存,占比为51.32%,而func2保留了19.33%
通过引用传递的范围循环变量给Go协程会导致内存泄漏。

英文:

Code for reproducing the problem

go version go1.19 darwin/amd64

package main

import (
	&quot;bytes&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	_ &quot;net/http/pprof&quot;
	&quot;runtime&quot;
	&quot;strconv&quot;
	&quot;sync&quot;
)

type TestStruct struct {
	M []byte
}

var lkMap sync.Mutex
var tMap map[string]*TestStruct

const objectCount = 1024 * 100

func main() {

	tMap = make(map[string]*TestStruct)

	wg := &amp;sync.WaitGroup{}
	wg.Add(objectCount)
	func1(wg)
	wg.Add(objectCount)
	func2(wg)

	wg.Wait()
	runtime.GC()
	log.Println(http.ListenAndServe(&quot;localhost:6060&quot;, nil))
}

//go:noinline
func func1(wg *sync.WaitGroup) {
	ts := []*TestStruct{}
	for i := 0; i &lt; objectCount; i++ {
		t := &amp;TestStruct{}
		ts = append(ts, t)
		lkMap.Lock()
		tMap[strconv.Itoa(i)] = t
		lkMap.Unlock()
	}

	for i, t := range ts {
		go func(t *TestStruct, idx int) {
			t.M = bytes.Repeat([]byte{byte(32)}, 1024)

			lkMap.Lock()
			delete(tMap, strconv.Itoa(idx))
			lkMap.Unlock()
			wg.Done()
		}(t, i) //pass by reference
	}
}

//go:noinline
func func2(wg *sync.WaitGroup) {
	ts := []*TestStruct{}
	for i := 0; i &lt; objectCount; i++ {
		t := &amp;TestStruct{}
		ts = append(ts, t)

		lkMap.Lock()
		tMap[strconv.Itoa(i+objectCount)] = t
		lkMap.Unlock()
	}
	for i, t := range ts {
		tmp := t //capture here
		idx := i
		go func() {
			tmp.M = bytes.Repeat([]byte{byte(32)}, 1024)

			lkMap.Lock()
			delete(tMap, strconv.Itoa(idx+objectCount))
			lkMap.Unlock()
			wg.Done()
		}()
	}
}


Run this program and using pprof to fetch the heap using the command line:

go tool pprof http://localhost:6060/debug/pprof/heap

will produce the diagram like this:
通过引用传递的范围循环变量给Go协程会导致内存泄漏。

As I can see the func1 is clearly leaking. The only difference between func1 and func2 is that func2 uses a local variable to capture the loop variable.

However, if I remove the global map both functions are fine, no one is leaking.

So is there something to do with the global map manipulation?

Thank you for any input

Edit:
Thanks @nipuna for pointing out that changing the order of func1 and func2 in main function will make func2 show in the report. However func1 is still hoarding the most memory which is 51.32% as func2 is retaining 19.33%
通过引用传递的范围循环变量给Go协程会导致内存泄漏。

答案1

得分: 2

我认为分析器图表在内存泄漏测试中并不是很有说服力。

Go运行时可以比分析器更好地了解其内存分配情况。可以尝试使用runtime.ReadMemStats函数输出堆内存的读数。

以下是我的实验代码:https://go.dev/play/p/279du2yB3BZ

输出结果如下:

Before func1: Heap Alloc: 260880 Heap objects: 1224
After func1:  Heap Alloc: 1317904 Heap objects: 2749
Before func1: Heap Alloc: 1319288 Heap objects: 2757
After func1:  Heap Alloc: 1576656 Heap objects: 3425
Before func1: Heap Alloc: 1578040 Heap objects: 3433
After func1:  Heap Alloc: 1858408 Heap objects: 4197
Before func1: Heap Alloc: 1859792 Heap objects: 4205
After func1:  Heap Alloc: 2210760 Heap objects: 4909
Before func1: Heap Alloc: 2212144 Heap objects: 4917
After func1:  Heap Alloc: 2213200 Heap objects: 4799
Before func1: Heap Alloc: 2214584 Heap objects: 4807
After func1:  Heap Alloc: 2247480 Heap objects: 5045
Before func1: Heap Alloc: 2248864 Heap objects: 5053
After func1:  Heap Alloc: 2298840 Heap objects: 5158
Before func1: Heap Alloc: 2300224 Heap objects: 5166
After func1:  Heap Alloc: 2297144 Heap objects: 5047
Before func1: Heap Alloc: 2298528 Heap objects: 5055
After func1:  Heap Alloc: 2316712 Heap objects: 5154
Before func1: Heap Alloc: 2318096 Heap objects: 5162
After func1:  Heap Alloc: 3083256 Heap objects: 7208
----------------------------
Before func2: Heap Alloc: 3084640 Heap objects: 7216
After func2:  Heap Alloc: 3075816 Heap objects: 7057
Before func2: Heap Alloc: 3077200 Heap objects: 7065
After func2:  Heap Alloc: 3102896 Heap objects: 7270
Before func2: Heap Alloc: 3104280 Heap objects: 7278
After func2:  Heap Alloc: 3127728 Heap objects: 7461
Before func2: Heap Alloc: 3129112 Heap objects: 7469
After func2:  Heap Alloc: 3128352 Heap objects: 7401
Before func2: Heap Alloc: 3129736 Heap objects: 7409
After func2:  Heap Alloc: 3121152 Heap objects: 7263
Before func2: Heap Alloc: 3122536 Heap objects: 7271
After func2:  Heap Alloc: 3167872 Heap objects: 7703
Before func2: Heap Alloc: 3169256 Heap objects: 7711
After func2:  Heap Alloc: 3163120 Heap objects: 7595
Before func2: Heap Alloc: 3164504 Heap objects: 7603
After func2:  Heap Alloc: 3157376 Heap objects: 7470
Before func2: Heap Alloc: 3158760 Heap objects: 7478
After func2:  Heap Alloc: 3160496 Heap objects: 7450
Before func2: Heap Alloc: 3161880 Heap objects: 7458
After func2:  Heap Alloc: 3221024 Heap objects: 7754
----------------------------
after GC:  Heap Alloc: 3222200 Heap objects: 7750
----------------------------

看起来你是对的——每次迭代堆中的对象数量都在不断增加。

但是,如果我们交换func1func2的顺序呢?让我们在调用func1之前先调用func2:https://go.dev/play/p/ABFq1O11bIl

输出结果如下:

Before func2: Heap Alloc: 261456 Heap objects: 1227
After func2:  Heap Alloc: 1330584 Heap objects: 2529
Before func2: Heap Alloc: 1331968 Heap objects: 2537
After func2:  Heap Alloc: 1739280 Heap objects: 3755
Before func2: Heap Alloc: 1740664 Heap objects: 3763
After func2:  Heap Alloc: 2251824 Heap objects: 5017
Before func2: Heap Alloc: 2253208 Heap objects: 5025
After func2:  Heap Alloc: 2244400 Heap objects: 4802
Before func2: Heap Alloc: 2245784 Heap objects: 4810
After func2:  Heap Alloc: 2287816 Heap objects: 5135
Before func2: Heap Alloc: 2289200 Heap objects: 5143
After func2:  Heap Alloc: 2726136 Heap objects: 6375
Before func2: Heap Alloc: 2727520 Heap objects: 6383
After func2:  Heap Alloc: 2834520 Heap objects: 6276
Before func2: Heap Alloc: 2835904 Heap objects: 6284
After func2:  Heap Alloc: 2855496 Heap objects: 6393
Before func2: Heap Alloc: 2856880 Heap objects: 6401
After func2:  Heap Alloc: 2873064 Heap objects: 6478
Before func2: Heap Alloc: 2874448 Heap objects: 6486
After func2:  Heap Alloc: 2923560 Heap objects: 6913
----------------------------
Before func1: Heap Alloc: 2924944 Heap objects: 6921
After func1:  Heap Alloc: 2933416 Heap objects: 6934
Before func1: Heap Alloc: 2934800 Heap objects: 6942
After func1:  Heap Alloc: 2916520 Heap objects: 6676
Before func1: Heap Alloc: 2917904 Heap objects: 6684
After func1:  Heap Alloc: 2941816 Heap objects: 6864
Before func1: Heap Alloc: 2943200 Heap objects: 6872
After func1:  Heap Alloc: 2968184 Heap objects: 7078
Before func1: Heap Alloc: 2969568 Heap objects: 7086
After func1:  Heap Alloc: 2955056 Heap objects: 6885
Before func1: Heap Alloc: 2956440 Heap objects: 6893
After func1:  Heap Alloc: 2961056 Heap objects: 6893
Before func1: Heap Alloc: 2962440 Heap objects: 6901
After func1:  Heap Alloc: 2967680 Heap objects: 6903
Before func1: Heap Alloc: 2969064 Heap objects: 6911
After func1:  Heap Alloc: 3005856 Heap objects: 7266
Before func1: Heap Alloc: 3007240 Heap objects: 7274
After func1:  Heap Alloc: 3033696 Heap objects: 7514
Before func1: Heap Alloc: 3035080 Heap objects: 7522
After func1:  Heap Alloc: 3028432 Heap objects: 7423
----------------------------
after GC:  Heap Alloc: 3029608 Heap objects: 7419
----------------------------

func2的堆几乎与func1一样快速增长,而func1处于第二个位置时的行为与func2完全相同——只向堆中添加了500个对象。

从这两个例子来看,我不能同意“func1泄漏而func2不泄漏”的说法。我认为它们泄漏的程度是相同的。

英文:

I don't think that profiler diagrams are any strong witnesses in the memory leak trial.

Go runtime can tell about its memory allocation much more than profiler. Try runtime.ReadMemStats to output heap readings.

Here is my experiment: https://go.dev/play/p/279du2yB3BZ

func printAllocInfo(message string) {
var memStats runtime.MemStats
runtime.ReadMemStats(&amp;memStats)
fmt.Println(message,
&quot;Heap Alloc:&quot;, memStats.HeapAlloc,
&quot;Heap objects:&quot;, memStats.HeapObjects)
}
func main() {
tMap = make(map[string]*TestStruct)
wg := &amp;sync.WaitGroup{}
for i := 0; i &lt; 10; i++ {
wg.Add(objectCount)
printAllocInfo(&quot;Before func2:&quot;)
func2(wg)
wg.Wait()
runtime.GC()
printAllocInfo(&quot;After func2: &quot;)
}
println(&quot;----------------------------&quot;)
for i := 0; i &lt; 10; i++ {
wg.Add(objectCount)
printAllocInfo(&quot;Before func1:&quot;)
func1(wg)
wg.Wait()
runtime.GC()
printAllocInfo(&quot;After func1: &quot;)
}
println(&quot;----------------------------&quot;)
runtime.GC()
printAllocInfo(&quot;after GC: &quot;)
println(&quot;----------------------------&quot;)
}

The output is

Before func1: Heap Alloc: 260880 Heap objects: 1224
After func1:  Heap Alloc: 1317904 Heap objects: 2749
Before func1: Heap Alloc: 1319288 Heap objects: 2757
After func1:  Heap Alloc: 1576656 Heap objects: 3425
Before func1: Heap Alloc: 1578040 Heap objects: 3433
After func1:  Heap Alloc: 1858408 Heap objects: 4197
Before func1: Heap Alloc: 1859792 Heap objects: 4205
After func1:  Heap Alloc: 2210760 Heap objects: 4909
Before func1: Heap Alloc: 2212144 Heap objects: 4917
After func1:  Heap Alloc: 2213200 Heap objects: 4799
Before func1: Heap Alloc: 2214584 Heap objects: 4807
After func1:  Heap Alloc: 2247480 Heap objects: 5045
Before func1: Heap Alloc: 2248864 Heap objects: 5053
After func1:  Heap Alloc: 2298840 Heap objects: 5158
Before func1: Heap Alloc: 2300224 Heap objects: 5166
After func1:  Heap Alloc: 2297144 Heap objects: 5047
Before func1: Heap Alloc: 2298528 Heap objects: 5055
After func1:  Heap Alloc: 2316712 Heap objects: 5154
Before func1: Heap Alloc: 2318096 Heap objects: 5162
After func1:  Heap Alloc: 3083256 Heap objects: 7208
----------------------------
Before func2: Heap Alloc: 3084640 Heap objects: 7216
After func2:  Heap Alloc: 3075816 Heap objects: 7057
Before func2: Heap Alloc: 3077200 Heap objects: 7065
After func2:  Heap Alloc: 3102896 Heap objects: 7270
Before func2: Heap Alloc: 3104280 Heap objects: 7278
After func2:  Heap Alloc: 3127728 Heap objects: 7461
Before func2: Heap Alloc: 3129112 Heap objects: 7469
After func2:  Heap Alloc: 3128352 Heap objects: 7401
Before func2: Heap Alloc: 3129736 Heap objects: 7409
After func2:  Heap Alloc: 3121152 Heap objects: 7263
Before func2: Heap Alloc: 3122536 Heap objects: 7271
After func2:  Heap Alloc: 3167872 Heap objects: 7703
Before func2: Heap Alloc: 3169256 Heap objects: 7711
After func2:  Heap Alloc: 3163120 Heap objects: 7595
Before func2: Heap Alloc: 3164504 Heap objects: 7603
After func2:  Heap Alloc: 3157376 Heap objects: 7470
Before func2: Heap Alloc: 3158760 Heap objects: 7478
After func2:  Heap Alloc: 3160496 Heap objects: 7450
Before func2: Heap Alloc: 3161880 Heap objects: 7458
After func2:  Heap Alloc: 3221024 Heap objects: 7754
----------------------------
after GC:  Heap Alloc: 3222200 Heap objects: 7750
----------------------------

Looks like you are right - with every iteration the number of object in the heap is constantly growing.

But what if we swap func1 and func2? Let's call func2 before func1: https://go.dev/play/p/ABFq1O11bIl

Before func2: Heap Alloc: 261456 Heap objects: 1227
After func2:  Heap Alloc: 1330584 Heap objects: 2529
Before func2: Heap Alloc: 1331968 Heap objects: 2537
After func2:  Heap Alloc: 1739280 Heap objects: 3755
Before func2: Heap Alloc: 1740664 Heap objects: 3763
After func2:  Heap Alloc: 2251824 Heap objects: 5017
Before func2: Heap Alloc: 2253208 Heap objects: 5025
After func2:  Heap Alloc: 2244400 Heap objects: 4802
Before func2: Heap Alloc: 2245784 Heap objects: 4810
After func2:  Heap Alloc: 2287816 Heap objects: 5135
Before func2: Heap Alloc: 2289200 Heap objects: 5143
After func2:  Heap Alloc: 2726136 Heap objects: 6375
Before func2: Heap Alloc: 2727520 Heap objects: 6383
After func2:  Heap Alloc: 2834520 Heap objects: 6276
Before func2: Heap Alloc: 2835904 Heap objects: 6284
After func2:  Heap Alloc: 2855496 Heap objects: 6393
Before func2: Heap Alloc: 2856880 Heap objects: 6401
After func2:  Heap Alloc: 2873064 Heap objects: 6478
Before func2: Heap Alloc: 2874448 Heap objects: 6486
After func2:  Heap Alloc: 2923560 Heap objects: 6913
----------------------------
Before func1: Heap Alloc: 2924944 Heap objects: 6921
After func1:  Heap Alloc: 2933416 Heap objects: 6934
Before func1: Heap Alloc: 2934800 Heap objects: 6942
After func1:  Heap Alloc: 2916520 Heap objects: 6676
Before func1: Heap Alloc: 2917904 Heap objects: 6684
After func1:  Heap Alloc: 2941816 Heap objects: 6864
Before func1: Heap Alloc: 2943200 Heap objects: 6872
After func1:  Heap Alloc: 2968184 Heap objects: 7078
Before func1: Heap Alloc: 2969568 Heap objects: 7086
After func1:  Heap Alloc: 2955056 Heap objects: 6885
Before func1: Heap Alloc: 2956440 Heap objects: 6893
After func1:  Heap Alloc: 2961056 Heap objects: 6893
Before func1: Heap Alloc: 2962440 Heap objects: 6901
After func1:  Heap Alloc: 2967680 Heap objects: 6903
Before func1: Heap Alloc: 2969064 Heap objects: 6911
After func1:  Heap Alloc: 3005856 Heap objects: 7266
Before func1: Heap Alloc: 3007240 Heap objects: 7274
After func1:  Heap Alloc: 3033696 Heap objects: 7514
Before func1: Heap Alloc: 3035080 Heap objects: 7522
After func1:  Heap Alloc: 3028432 Heap objects: 7423
----------------------------
after GC:  Heap Alloc: 3029608 Heap objects: 7419
----------------------------

Heap of func2 grows almost as fast as with func1, and func1 being in the second place behaves exactly like func2 - adds just 500 objects to the heap.

From these two examples I can't agree that func1 is leaking while func2 is not. I'd say, they are leaking to the same degree.

答案2

得分: 1

感谢golang-nuts小组的回答,以下是引用的内容:

> 我认为这与for-range循环无关,两者应该“泄漏”相同的内存。我敢打赌是tMap的问题,目前在Go中,即使删除了键,映射也不会收缩。减少长期存在的映射的内存使用的一种方法是,在某个阈值上将其所有元素复制到一个新的映射中,并将旧映射设置为指向此副本,然后如果旧映射在其他地方没有被引用,它最终将被垃圾回收并清理。

根本原因是长期存在的映射。

我还在这里找到了该问题的链接:
https://github.com/golang/go/issues/20135

英文:

Thanks for the answer from golang-nuts group, quote here:

> I don't think it's related with for-range loops, both should "leak"
the same, I bet that's tMap, currently in Go maps do not shrink, even
if you delete keys. One way to reduce memory usage of a long-living
map is to at some threshold copy over all its elements into a new map,
and set the old to map to this copy, then the old map, if not
referenced elsewhere, will eventually be gc'ed and cleaned up.

The root cause is the long-lived map

I also found the issue here:
https://github.com/golang/go/issues/20135

huangapple
  • 本文由 发表于 2022年10月7日 18:29:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/73985794.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定