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

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

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

问题

用于复现问题的代码

go版本 go1.19 darwin/amd64

  1. package main
  2. import (
  3. "bytes"
  4. "log"
  5. "net/http"
  6. _ "net/http/pprof"
  7. "runtime"
  8. "strconv"
  9. "sync"
  10. )
  11. type TestStruct struct {
  12. M []byte
  13. }
  14. var lkMap sync.Mutex
  15. var tMap map[string]*TestStruct
  16. const objectCount = 1024 * 100
  17. func main() {
  18. tMap = make(map[string]*TestStruct)
  19. wg := &sync.WaitGroup{}
  20. wg.Add(objectCount)
  21. func1(wg)
  22. wg.Add(objectCount)
  23. func2(wg)
  24. wg.Wait()
  25. runtime.GC()
  26. log.Println(http.ListenAndServe("localhost:6060", nil))
  27. }
  28. //go:noinline
  29. func func1(wg *sync.WaitGroup) {
  30. ts := []*TestStruct{}
  31. for i := 0; i < objectCount; i++ {
  32. t := &TestStruct{}
  33. ts = append(ts, t)
  34. lkMap.Lock()
  35. tMap[strconv.Itoa(i)] = t
  36. lkMap.Unlock()
  37. }
  38. for i, t := range ts {
  39. go func(t *TestStruct, idx int) {
  40. t.M = bytes.Repeat([]byte{byte(32)}, 1024)
  41. lkMap.Lock()
  42. delete(tMap, strconv.Itoa(idx))
  43. lkMap.Unlock()
  44. wg.Done()
  45. }(t, i) //pass by reference
  46. }
  47. }
  48. //go:noinline
  49. func func2(wg *sync.WaitGroup) {
  50. ts := []*TestStruct{}
  51. for i := 0; i < objectCount; i++ {
  52. t := &TestStruct{}
  53. ts = append(ts, t)
  54. lkMap.Lock()
  55. tMap[strconv.Itoa(i+objectCount)] = t
  56. lkMap.Unlock()
  57. }
  58. for i, t := range ts {
  59. tmp := t //capture here
  60. idx := i
  61. go func() {
  62. tmp.M = bytes.Repeat([]byte{byte(32)}, 1024)
  63. lkMap.Lock()
  64. delete(tMap, strconv.Itoa(idx+objectCount))
  65. lkMap.Unlock()
  66. wg.Done()
  67. }()
  68. }
  69. }

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

  1. 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

  1. package main
  2. import (
  3. &quot;bytes&quot;
  4. &quot;log&quot;
  5. &quot;net/http&quot;
  6. _ &quot;net/http/pprof&quot;
  7. &quot;runtime&quot;
  8. &quot;strconv&quot;
  9. &quot;sync&quot;
  10. )
  11. type TestStruct struct {
  12. M []byte
  13. }
  14. var lkMap sync.Mutex
  15. var tMap map[string]*TestStruct
  16. const objectCount = 1024 * 100
  17. func main() {
  18. tMap = make(map[string]*TestStruct)
  19. wg := &amp;sync.WaitGroup{}
  20. wg.Add(objectCount)
  21. func1(wg)
  22. wg.Add(objectCount)
  23. func2(wg)
  24. wg.Wait()
  25. runtime.GC()
  26. log.Println(http.ListenAndServe(&quot;localhost:6060&quot;, nil))
  27. }
  28. //go:noinline
  29. func func1(wg *sync.WaitGroup) {
  30. ts := []*TestStruct{}
  31. for i := 0; i &lt; objectCount; i++ {
  32. t := &amp;TestStruct{}
  33. ts = append(ts, t)
  34. lkMap.Lock()
  35. tMap[strconv.Itoa(i)] = t
  36. lkMap.Unlock()
  37. }
  38. for i, t := range ts {
  39. go func(t *TestStruct, idx int) {
  40. t.M = bytes.Repeat([]byte{byte(32)}, 1024)
  41. lkMap.Lock()
  42. delete(tMap, strconv.Itoa(idx))
  43. lkMap.Unlock()
  44. wg.Done()
  45. }(t, i) //pass by reference
  46. }
  47. }
  48. //go:noinline
  49. func func2(wg *sync.WaitGroup) {
  50. ts := []*TestStruct{}
  51. for i := 0; i &lt; objectCount; i++ {
  52. t := &amp;TestStruct{}
  53. ts = append(ts, t)
  54. lkMap.Lock()
  55. tMap[strconv.Itoa(i+objectCount)] = t
  56. lkMap.Unlock()
  57. }
  58. for i, t := range ts {
  59. tmp := t //capture here
  60. idx := i
  61. go func() {
  62. tmp.M = bytes.Repeat([]byte{byte(32)}, 1024)
  63. lkMap.Lock()
  64. delete(tMap, strconv.Itoa(idx+objectCount))
  65. lkMap.Unlock()
  66. wg.Done()
  67. }()
  68. }
  69. }

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

  1. 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

输出结果如下:

  1. Before func1: Heap Alloc: 260880 Heap objects: 1224
  2. After func1: Heap Alloc: 1317904 Heap objects: 2749
  3. Before func1: Heap Alloc: 1319288 Heap objects: 2757
  4. After func1: Heap Alloc: 1576656 Heap objects: 3425
  5. Before func1: Heap Alloc: 1578040 Heap objects: 3433
  6. After func1: Heap Alloc: 1858408 Heap objects: 4197
  7. Before func1: Heap Alloc: 1859792 Heap objects: 4205
  8. After func1: Heap Alloc: 2210760 Heap objects: 4909
  9. Before func1: Heap Alloc: 2212144 Heap objects: 4917
  10. After func1: Heap Alloc: 2213200 Heap objects: 4799
  11. Before func1: Heap Alloc: 2214584 Heap objects: 4807
  12. After func1: Heap Alloc: 2247480 Heap objects: 5045
  13. Before func1: Heap Alloc: 2248864 Heap objects: 5053
  14. After func1: Heap Alloc: 2298840 Heap objects: 5158
  15. Before func1: Heap Alloc: 2300224 Heap objects: 5166
  16. After func1: Heap Alloc: 2297144 Heap objects: 5047
  17. Before func1: Heap Alloc: 2298528 Heap objects: 5055
  18. After func1: Heap Alloc: 2316712 Heap objects: 5154
  19. Before func1: Heap Alloc: 2318096 Heap objects: 5162
  20. After func1: Heap Alloc: 3083256 Heap objects: 7208
  21. ----------------------------
  22. Before func2: Heap Alloc: 3084640 Heap objects: 7216
  23. After func2: Heap Alloc: 3075816 Heap objects: 7057
  24. Before func2: Heap Alloc: 3077200 Heap objects: 7065
  25. After func2: Heap Alloc: 3102896 Heap objects: 7270
  26. Before func2: Heap Alloc: 3104280 Heap objects: 7278
  27. After func2: Heap Alloc: 3127728 Heap objects: 7461
  28. Before func2: Heap Alloc: 3129112 Heap objects: 7469
  29. After func2: Heap Alloc: 3128352 Heap objects: 7401
  30. Before func2: Heap Alloc: 3129736 Heap objects: 7409
  31. After func2: Heap Alloc: 3121152 Heap objects: 7263
  32. Before func2: Heap Alloc: 3122536 Heap objects: 7271
  33. After func2: Heap Alloc: 3167872 Heap objects: 7703
  34. Before func2: Heap Alloc: 3169256 Heap objects: 7711
  35. After func2: Heap Alloc: 3163120 Heap objects: 7595
  36. Before func2: Heap Alloc: 3164504 Heap objects: 7603
  37. After func2: Heap Alloc: 3157376 Heap objects: 7470
  38. Before func2: Heap Alloc: 3158760 Heap objects: 7478
  39. After func2: Heap Alloc: 3160496 Heap objects: 7450
  40. Before func2: Heap Alloc: 3161880 Heap objects: 7458
  41. After func2: Heap Alloc: 3221024 Heap objects: 7754
  42. ----------------------------
  43. after GC: Heap Alloc: 3222200 Heap objects: 7750
  44. ----------------------------

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

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

输出结果如下:

  1. Before func2: Heap Alloc: 261456 Heap objects: 1227
  2. After func2: Heap Alloc: 1330584 Heap objects: 2529
  3. Before func2: Heap Alloc: 1331968 Heap objects: 2537
  4. After func2: Heap Alloc: 1739280 Heap objects: 3755
  5. Before func2: Heap Alloc: 1740664 Heap objects: 3763
  6. After func2: Heap Alloc: 2251824 Heap objects: 5017
  7. Before func2: Heap Alloc: 2253208 Heap objects: 5025
  8. After func2: Heap Alloc: 2244400 Heap objects: 4802
  9. Before func2: Heap Alloc: 2245784 Heap objects: 4810
  10. After func2: Heap Alloc: 2287816 Heap objects: 5135
  11. Before func2: Heap Alloc: 2289200 Heap objects: 5143
  12. After func2: Heap Alloc: 2726136 Heap objects: 6375
  13. Before func2: Heap Alloc: 2727520 Heap objects: 6383
  14. After func2: Heap Alloc: 2834520 Heap objects: 6276
  15. Before func2: Heap Alloc: 2835904 Heap objects: 6284
  16. After func2: Heap Alloc: 2855496 Heap objects: 6393
  17. Before func2: Heap Alloc: 2856880 Heap objects: 6401
  18. After func2: Heap Alloc: 2873064 Heap objects: 6478
  19. Before func2: Heap Alloc: 2874448 Heap objects: 6486
  20. After func2: Heap Alloc: 2923560 Heap objects: 6913
  21. ----------------------------
  22. Before func1: Heap Alloc: 2924944 Heap objects: 6921
  23. After func1: Heap Alloc: 2933416 Heap objects: 6934
  24. Before func1: Heap Alloc: 2934800 Heap objects: 6942
  25. After func1: Heap Alloc: 2916520 Heap objects: 6676
  26. Before func1: Heap Alloc: 2917904 Heap objects: 6684
  27. After func1: Heap Alloc: 2941816 Heap objects: 6864
  28. Before func1: Heap Alloc: 2943200 Heap objects: 6872
  29. After func1: Heap Alloc: 2968184 Heap objects: 7078
  30. Before func1: Heap Alloc: 2969568 Heap objects: 7086
  31. After func1: Heap Alloc: 2955056 Heap objects: 6885
  32. Before func1: Heap Alloc: 2956440 Heap objects: 6893
  33. After func1: Heap Alloc: 2961056 Heap objects: 6893
  34. Before func1: Heap Alloc: 2962440 Heap objects: 6901
  35. After func1: Heap Alloc: 2967680 Heap objects: 6903
  36. Before func1: Heap Alloc: 2969064 Heap objects: 6911
  37. After func1: Heap Alloc: 3005856 Heap objects: 7266
  38. Before func1: Heap Alloc: 3007240 Heap objects: 7274
  39. After func1: Heap Alloc: 3033696 Heap objects: 7514
  40. Before func1: Heap Alloc: 3035080 Heap objects: 7522
  41. After func1: Heap Alloc: 3028432 Heap objects: 7423
  42. ----------------------------
  43. after GC: Heap Alloc: 3029608 Heap objects: 7419
  44. ----------------------------

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

  1. func printAllocInfo(message string) {
  2. var memStats runtime.MemStats
  3. runtime.ReadMemStats(&amp;memStats)
  4. fmt.Println(message,
  5. &quot;Heap Alloc:&quot;, memStats.HeapAlloc,
  6. &quot;Heap objects:&quot;, memStats.HeapObjects)
  7. }
  8. func main() {
  9. tMap = make(map[string]*TestStruct)
  10. wg := &amp;sync.WaitGroup{}
  11. for i := 0; i &lt; 10; i++ {
  12. wg.Add(objectCount)
  13. printAllocInfo(&quot;Before func2:&quot;)
  14. func2(wg)
  15. wg.Wait()
  16. runtime.GC()
  17. printAllocInfo(&quot;After func2: &quot;)
  18. }
  19. println(&quot;----------------------------&quot;)
  20. for i := 0; i &lt; 10; i++ {
  21. wg.Add(objectCount)
  22. printAllocInfo(&quot;Before func1:&quot;)
  23. func1(wg)
  24. wg.Wait()
  25. runtime.GC()
  26. printAllocInfo(&quot;After func1: &quot;)
  27. }
  28. println(&quot;----------------------------&quot;)
  29. runtime.GC()
  30. printAllocInfo(&quot;after GC: &quot;)
  31. println(&quot;----------------------------&quot;)
  32. }

The output is

  1. Before func1: Heap Alloc: 260880 Heap objects: 1224
  2. After func1: Heap Alloc: 1317904 Heap objects: 2749
  3. Before func1: Heap Alloc: 1319288 Heap objects: 2757
  4. After func1: Heap Alloc: 1576656 Heap objects: 3425
  5. Before func1: Heap Alloc: 1578040 Heap objects: 3433
  6. After func1: Heap Alloc: 1858408 Heap objects: 4197
  7. Before func1: Heap Alloc: 1859792 Heap objects: 4205
  8. After func1: Heap Alloc: 2210760 Heap objects: 4909
  9. Before func1: Heap Alloc: 2212144 Heap objects: 4917
  10. After func1: Heap Alloc: 2213200 Heap objects: 4799
  11. Before func1: Heap Alloc: 2214584 Heap objects: 4807
  12. After func1: Heap Alloc: 2247480 Heap objects: 5045
  13. Before func1: Heap Alloc: 2248864 Heap objects: 5053
  14. After func1: Heap Alloc: 2298840 Heap objects: 5158
  15. Before func1: Heap Alloc: 2300224 Heap objects: 5166
  16. After func1: Heap Alloc: 2297144 Heap objects: 5047
  17. Before func1: Heap Alloc: 2298528 Heap objects: 5055
  18. After func1: Heap Alloc: 2316712 Heap objects: 5154
  19. Before func1: Heap Alloc: 2318096 Heap objects: 5162
  20. After func1: Heap Alloc: 3083256 Heap objects: 7208
  21. ----------------------------
  22. Before func2: Heap Alloc: 3084640 Heap objects: 7216
  23. After func2: Heap Alloc: 3075816 Heap objects: 7057
  24. Before func2: Heap Alloc: 3077200 Heap objects: 7065
  25. After func2: Heap Alloc: 3102896 Heap objects: 7270
  26. Before func2: Heap Alloc: 3104280 Heap objects: 7278
  27. After func2: Heap Alloc: 3127728 Heap objects: 7461
  28. Before func2: Heap Alloc: 3129112 Heap objects: 7469
  29. After func2: Heap Alloc: 3128352 Heap objects: 7401
  30. Before func2: Heap Alloc: 3129736 Heap objects: 7409
  31. After func2: Heap Alloc: 3121152 Heap objects: 7263
  32. Before func2: Heap Alloc: 3122536 Heap objects: 7271
  33. After func2: Heap Alloc: 3167872 Heap objects: 7703
  34. Before func2: Heap Alloc: 3169256 Heap objects: 7711
  35. After func2: Heap Alloc: 3163120 Heap objects: 7595
  36. Before func2: Heap Alloc: 3164504 Heap objects: 7603
  37. After func2: Heap Alloc: 3157376 Heap objects: 7470
  38. Before func2: Heap Alloc: 3158760 Heap objects: 7478
  39. After func2: Heap Alloc: 3160496 Heap objects: 7450
  40. Before func2: Heap Alloc: 3161880 Heap objects: 7458
  41. After func2: Heap Alloc: 3221024 Heap objects: 7754
  42. ----------------------------
  43. after GC: Heap Alloc: 3222200 Heap objects: 7750
  44. ----------------------------

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

  1. Before func2: Heap Alloc: 261456 Heap objects: 1227
  2. After func2: Heap Alloc: 1330584 Heap objects: 2529
  3. Before func2: Heap Alloc: 1331968 Heap objects: 2537
  4. After func2: Heap Alloc: 1739280 Heap objects: 3755
  5. Before func2: Heap Alloc: 1740664 Heap objects: 3763
  6. After func2: Heap Alloc: 2251824 Heap objects: 5017
  7. Before func2: Heap Alloc: 2253208 Heap objects: 5025
  8. After func2: Heap Alloc: 2244400 Heap objects: 4802
  9. Before func2: Heap Alloc: 2245784 Heap objects: 4810
  10. After func2: Heap Alloc: 2287816 Heap objects: 5135
  11. Before func2: Heap Alloc: 2289200 Heap objects: 5143
  12. After func2: Heap Alloc: 2726136 Heap objects: 6375
  13. Before func2: Heap Alloc: 2727520 Heap objects: 6383
  14. After func2: Heap Alloc: 2834520 Heap objects: 6276
  15. Before func2: Heap Alloc: 2835904 Heap objects: 6284
  16. After func2: Heap Alloc: 2855496 Heap objects: 6393
  17. Before func2: Heap Alloc: 2856880 Heap objects: 6401
  18. After func2: Heap Alloc: 2873064 Heap objects: 6478
  19. Before func2: Heap Alloc: 2874448 Heap objects: 6486
  20. After func2: Heap Alloc: 2923560 Heap objects: 6913
  21. ----------------------------
  22. Before func1: Heap Alloc: 2924944 Heap objects: 6921
  23. After func1: Heap Alloc: 2933416 Heap objects: 6934
  24. Before func1: Heap Alloc: 2934800 Heap objects: 6942
  25. After func1: Heap Alloc: 2916520 Heap objects: 6676
  26. Before func1: Heap Alloc: 2917904 Heap objects: 6684
  27. After func1: Heap Alloc: 2941816 Heap objects: 6864
  28. Before func1: Heap Alloc: 2943200 Heap objects: 6872
  29. After func1: Heap Alloc: 2968184 Heap objects: 7078
  30. Before func1: Heap Alloc: 2969568 Heap objects: 7086
  31. After func1: Heap Alloc: 2955056 Heap objects: 6885
  32. Before func1: Heap Alloc: 2956440 Heap objects: 6893
  33. After func1: Heap Alloc: 2961056 Heap objects: 6893
  34. Before func1: Heap Alloc: 2962440 Heap objects: 6901
  35. After func1: Heap Alloc: 2967680 Heap objects: 6903
  36. Before func1: Heap Alloc: 2969064 Heap objects: 6911
  37. After func1: Heap Alloc: 3005856 Heap objects: 7266
  38. Before func1: Heap Alloc: 3007240 Heap objects: 7274
  39. After func1: Heap Alloc: 3033696 Heap objects: 7514
  40. Before func1: Heap Alloc: 3035080 Heap objects: 7522
  41. After func1: Heap Alloc: 3028432 Heap objects: 7423
  42. ----------------------------
  43. after GC: Heap Alloc: 3029608 Heap objects: 7419
  44. ----------------------------

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:

确定