Golang中如何让协程释放已使用的内存

Golang中如何让协程释放已使用的内存 我使用Go语言创建了一个多线程的孪生素数筛,以便与其他实现进行比较。 我意识到Go语言没有真正的并行性,而是使用Go协程实现并发。 我遇到的问题是,每个Go协程似乎会一直占用其使用的内存,直到所有线程/进程结束,而不是在其特定进程结束时释放内存。 随着Go协程数量的增加,这会导致程序消耗越来越多的内存。 随着输入值增大,会使用更多内存,直到程序因超出可用内存而崩溃。

以下是执行Go协程的代码部分结尾。

  sums := make([]uint, pairscnt)
  lastwins := make([]uint, pairscnt)

  var wg sync.WaitGroup
  for i, r_hi := range restwins {
    wg.Add(1)
    go func(i, r_hi int) {
      defer wg.Done()
      l, c := twins_sieve(r_hi, kmin, kmax, kb, start_num, end_num, modpg, primes, resinvrs)
      lastwins[i] = l; sumss[i] = c
      fmt.Printf("\r%d of %d twinpairs done", (i + 1), pairscnt)
    }(i, r_hi)
  }
  wg.Wait()

以下是完整代码的要点。

你可以编译并运行它,观察其内存使用情况,例如使用htop,随着输入的增加。 尽管我有16GB内存,但随着输入值的增加,内存很快就会被耗尽。

有没有办法让Go协程在处理结束时立即释放内存?

https://gist.github.com/jzakiya/fbc77b8fdd12b0581a0ff7c2476373d9

twinprimes_ssoz.go

// This Go source file is a multiple threaded implementation to perform an
// extremely fast Segmented Sieve of Zakiya (SSoZ) to find Twin Primes <= N.

// Inputs are single values N, or ranges N1 and N2, of 64-bits, 0 -- 2^64 - 1.
// Output is the number of twin primes <= N, or in range N1 to N2; the last
// twin prime value for the range; and the total time of execution.

// This code was developed on a System76 laptop with an Intel I7 6700HQ cpu,
// 2.6-3.5 GHz clock, with 8 threads, and 16GB of memory. Parameter tuning
// probably needed to optimize for other hardware systems (ARM, PowerPC, etc).

该文件已被截断。显示原文

Twin Primes Segmented Sieve of Zakiys (SSoZ) Explained


更多关于Golang中如何让协程释放已使用的内存的实战教程也可以访问 https://www.itying.com/category-94-b0.html

21 回复

我正在使用 1.16 版本。

更多关于Golang中如何让协程释放已使用的内存的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好,你使用的是哪个版本的 Go?

你没有充分发挥 Go 语言的优势。

(此帖因违反行为准则已被移除)

func main() {
    fmt.Println("hello world")
}

找点正经事做吧,白痴。你既小气又愚蠢。

我正在尝试使用Go来实现一个我感兴趣的算法。 这为我提供了学习它的理由,并可以与其他语言进行比较。 众所周知,不同的语言(工具)在某些方面各有所长。 我试图确定Go在执行此算法上是否能比我已实现的其他方案做得更好。

如果你今天心情不好……请不要把情绪带到这里。你的回答毫无帮助。 他究竟在哪些方面没有充分利用 Go 的优势?请明确指出。 你只是在发表一些毫无帮助、居高临下的言论。放松点,冷静一下。我们也是来这里学习的。

如果您感到被冒犯,请使用论坛的举报机制。总的来说,我们彼此尊重,尽管我目前不知道有(成文的)行为准则。

要举报帖子,请点击帖子上的三个点图标,然后点击旗帜图标。

不过我得说,我习惯了 petrus 以往更多的善意和条理清晰的解释,不确定最近几天是什么让他变成这样……

是的,我在使用Linux系统。

我的电脑有16GB内存,处理器是i7(4核8线程),运行着Go 1.16。

随着输入大小的增加,会占用更多内存。 请注意内存使用在以下区间出现峰值:15,000,000,000,000 到 16,000,000,000,000 此时程序切换到了一个更大的生成器,该生成器会创建22,275个线程。

你假设 Go 协程在完成后没有释放内存。所以你也在做假设。

我目前没有时间和手段亲自测试你的代码并调查这个问题。我为此表示歉意。

对我来说,如果说有什么羞耻的话,那就是你理所当然地认为我们会运行你的代码,找出你看到的内存问题的原因,并且如果我们不这样做,就称之为羞耻。

我可能会在本周末看一看。问题的原因可能并不容易找到。

你是打算给我们提供精确的指令,告诉我们如何运行你那堆密集的代码来复现你的问题,还是说这是个猜谜游戏?

我猜你并不理解并发、调度和协程。如果你启动 1,000,000 个 CPU 密集型的协程并发运行,那么你将需要为多达 1,000,000 个协程分配内存,这很可能超出了你实际的物理内存总量。至少,你会无情地让你的分页和交换数据集承受巨大的压力。

这个论坛是否有行为准则政策?

@petrus 持续发表贬低、不尊重和破坏性的评论,这为我和其他人营造了一种充满敌意和不友好的环境。

为什么他被允许这样做?

我是 Go 语言的新手,基于我在这里受到的对待,以及我观察到的他与他人的互动方式,我不得不考虑是否要继续参与这个论坛,尽管我认为这门语言本身有很多优点。

但人们会根据你如何对待他人来评判你。

我现在有时间来研究你的问题了。你能提供导致问题出现的输入值吗?

我测试了 echo 10000000 | TestPrimesecho 100000 1000000 | TestPrimes,没有发现任何问题。程序立即终止,输出看起来正常。

我还测试了 echo 1 100000000000 | TestPrimes,这需要更长的时间来完成,但仍然没有内存问题。

你是在 Windows 还是 Linux 上运行代码?我是在 Linux 上运行代码的。我假设是 Linux,因为你建议使用 htop。但我无法复现这个问题。

今天在另一个论坛看到了这个关于 go routines 的帖子,

https://forum.nim-lang.org/t/7667

其中引用了这篇文章:

https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/

我不确定文章中提出的观点与我观察到的现象有多大关联,但似乎可能有关,因为涉及到 go routines 中已使用内存的保留问题。

我怀疑你遇到了内存泄漏,因为如果你的假设成立,很多人应该早就抱怨这个问题了。你使用 goroutine 的方式是标准且基础的。

所以对我来说,问题归结为“内存泄漏在哪里?”。

启动一百万个 goroutine 是完全合理的。它们不会同时启动。实际运行的 goroutine 数量会受到 CPU 核心数的限制。这是 Go 自动为你处理的。

Go 以最优的方式实现真正的并行,并将复杂且难以正确处理的技术细节隐藏起来。内存释放由垃圾收集器管理,它应该能够处理你给出的这类任务,除非存在内存泄漏。在 Go 中,内存泄漏通常是指你认为不再被引用的数据实际上仍然被引用,或者某个缓冲区无限增长。

你确定你展示的代码就是产生问题的代码吗?sumsS 有一个拼写错误。显示的代码将无法编译。

抱歉,由于时间关系,我没有查看 gist 中的代码。

我的内存只有8GB。当输入 echo 1 100000000000000 | TestPrimes 这个值时,进程会以状态码137崩溃。显然,这是操作系统因为进程使用了过多内存而将其终止。请注意,这里有14个零。

随后,我通过减少可以并发运行的操作系统线程数量进行了测试。Go协程是由操作系统线程运行的绿色线程。使用 GOMAXPROCS=2;(echo 1 100000000000000 | TestPrimes)。没有发生崩溃。

在不指定 GOMAXPROCS 的情况下,我在 htop 中看到5个 TestPrimes 线程。我原本预期是4个,因为系统有4个核心。程序输出的是“threads = 4”。

通过指定 GOMAXPROCS=2,我只看到三个线程,但程序仍然输出“threads = 4”。在这种配置下,系统没有耗尽内存。

我怀疑,当有5个操作系统级别的 TestPrimes 线程时,垃圾回收器无法充分运行,这或许可以解释为什么会耗尽内存。我需要向专家请教这是否正确。

petrus:

你是打算给我们提供精确的指令来运行你那堆密集的代码以复现你的问题,还是说这是个猜谜游戏?

我猜你并不理解并发、调度和 goroutine。如果你启动 1,000,000 个受 CPU 限制的 goroutine 并发运行,那么你将需要为多达 1,000,000 个 goroutine 分配内存,这很可能超出了你的实际总内存。至少,你会无情地让你的分页和交换数据集剧烈抖动。

哇,你这话里可带着不少敌意啊。

所有编译和运行代码的说明都在代码顶部的注释里。 你看了吗?

我个人目前已经在 D、C++、Nim、Rust 和 Crystal 中实现了这个算法,其他语言都没有像 Go 实现这样消耗内存。所以问题不在我,问题在于 Go 做了什么导致它消耗内存,以及如何修复它使其不这样。

如果你不知道怎么做,没关系。也许其他人知道。如果其他人都做不到,那么 Go 只是不适合这个用例而已,生活还得继续。

Christophe_Meessen:

你确定你展示的代码就是产生问题的那个吗?sumsS 有一个拼写错误。展示的代码无法编译。

抱歉,由于时间关系,我没有查看 gist 中的代码。

是的,sumss 是那个片段中的一个拼写错误,它应该是 sums,正如你意识到的,但代码是能工作的。

你对我可能做错了什么导致某种假设的内存泄漏做出了所有这些论断,但随后又承认你从未费心去运行 gist 中能重现我所说问题的代码,这真的很有趣。

同样的算法现在已经在 D、C++、Nim、Java、Rust 和 Crystal 中实现过了,而 Go 是唯一表现出这个问题的语言。问题不在于我,问题不在于算法,问题在于 Go。

这个论坛是我经历过的最奇怪的语言论坛。

我提出了一个问题,我提供了产生这个问题的代码,但显然,没有人去运行代码以亲眼看到问题,所有的评论都是关于我,或者我(或没有)做错的事情。

我只能假设这里的人只是对理解 Go 中是什么导致了这个问题并修复它不感兴趣。真遗憾。

经过进一步调查,GOMAXPROCS 并没有真正起到帮助,但怀疑的原因得到了证实。

根据我现在的理解,发生的情况是:你启动了许多协程,它们都会分配内存块。Go 语言启动了这些协程,但每个协程只分到一小部分 CPU 时间。于是,你有了许多并发运行但并非并行执行的协程。垃圾回收器无法应对需要完成的工作,无法回收所有内存。系统变得拥塞并最终内存耗尽。

我尝试添加 sync.Pool 来回收缓冲区,一旦缓冲区被回收,效果确实非常显著。不幸的是,在回收机制生效之前,仍然启动了太多的协程。

我测试的另一种策略是限制允许并发运行的协程数量。这个方法是有效的。它看起来似乎更慢,但实际上更快,因为你没有成千上万的协程在竞争 CPU。这段代码不会崩溃。

你必须导入这个包 github.com/korovkin/limiter

以下是代码:

	limit := limiter.NewConcurrencyLimiter(10)
	fmt.Printf("ChM: [main] restwins=%d\n", len(restwins))
	for i, r_hi := range restwins { // sieve twinpair restracks
		limit.Execute(func() {
			fmt.Printf("execute %d cnt: %v\n", i, limit.GetNumInProgress())
			l, c := twins_sieve(r_hi, kmin, kmax, kb, start_num, end_num, modpg, primes, resinvrs)
			lastwins[i] = l
			■■■■[i] = c
			fmt.Printf("%d of %d twinpairs done\n", (i + 1), pairscnt)
		})
	}
	limit.Wait()
	fmt.Printf("\r%d of %d twinpairs done", pairscnt, pairscnt)

我用值 1 和 100000000000000 进行了测试。在此之前,程序会崩溃或导致系统冻结。现在它不会崩溃了。你可以调整数值 10 来观察它对速度的影响。

希望这能有所帮助。

在Go中,协程的内存管理由垃圾收集器(GC)自动处理。协程本身不会“持有”内存,但协程栈和堆上分配的对象会持续存在,直到不再被引用。您遇到的内存问题可能源于以下几个方面:

  1. 切片sumsslastwins在协程结束后仍持有引用:这两个切片在协程中被赋值,但切片本身在父协程中持续存在,导致底层数组无法被释放。
  2. 协程栈未及时收缩:Go协程栈默认从2KB开始,可能随执行增长,但栈收缩不是实时的。
  3. twins_sieve函数内部可能分配了大量临时内存:这些内存在函数返回后应被释放,但GC触发时机不确定。

以下是针对您代码的修改建议,重点在于减少内存驻留时间:

sums := make([]uint, pairscnt)
lastwins := make([]uint, pairscnt)

var wg sync.WaitGroup
for i, r_hi := range restwins {
    wg.Add(1)
    go func(i, r_hi int) {
        defer wg.Done()
        // 将结果直接写入切片,避免额外分配
        l, c := twins_sieve(r_hi, kmin, kmax, kb, start_num, end_num, modpg, primes, resinvrs)
        lastwins[i] = l
        sums[i] = c
        fmt.Printf("\r%d of %d twinpairs done", (i+1), pairscnt)
        // 显式触发GC(谨慎使用,仅用于测试)
        // runtime.GC()
    }(i, r_hi)
}
wg.Wait()

更关键的优化在于twins_sieve函数内部。确保该函数:

  • 避免全局或包级变量缓存中间结果
  • 使用局部变量而非持久化对象
  • 对于大型临时数据结构,使用sync.Pool复用内存

示例使用sync.Pool减少分配:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 0, 1024*1024) // 预分配1MB缓冲区
    },
}

func twins_sieve(...) (uint, uint) {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf[:0]) // 重置并放回池中
    
    // 使用buf进行计算...
    // ... 函数逻辑 ...
    
    return l, c
}

此外,调整GC参数可能改善内存行为:

// 程序启动时设置
func init() {
    // 降低GC触发阈值,更频繁回收(增加CPU开销)
    debug.SetGCPercent(20) // 默认100
}

最后,验证primesresinvrs等传入参数是否在协程间共享只读数据。如果是,确保它们是不可变的,避免意外引用延长生命周期。

实际内存问题通常需用pprof分析:

import _ "net/http/pprof"

// 在main函数中添加
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

运行后访问http://localhost:6060/debug/pprof/heap获取堆分析数据。

回到顶部