【问题标题】:GC not able to collect back memory using fork-emulation on WindowsGC 无法在 Windows 上使用 fork-emulation 收集回内存
【发布时间】:2014-09-17 15:34:25
【问题描述】:

首先让我说我对 Perl 没有深入的了解,所以如果我遗漏了一些明显的东西,请原谅我:)

在我正在查看的系统(在 Windows 环境中运行)中,我们有一个 perl 进程,它必须下载 ~5000-6000 个文件。由于每个文件都可以独立下载,我们为每个文件分叉了单独的线程。该线程应该下载文件并死亡。在运行该进程时,我注意到该进程的内存上升到 ~1.7 GB,然后由于每个进程的内存限制而死掉。

在搜索和询问一些人时,我遇到了循环引用的概念,因为垃圾收集器不会释放内存。我搜索了一下,找到了Devel-Cycle 包,它可以找出对象中是否有任何循环。我得到了这个包并添加了一行来检查进程中的主要对象是否有任何循环。 find_cycle 为每个线程返回了以下语句。

DBD::Oracle::db FIRSTKEY failed: handle 2 is owned by thread 256004 not current thread c0ea29c (handles can't be shared between threads and your driver may need a CLONE method added) at C:/Program Files/Perl/site/lib/Devel/Cycle.pm line 151.

我知道数据库句柄不能在线程之间共享。我再次查看代码并意识到在 fork 发生后,子进程确实创建了一个新的 DB 句柄(我猜这就是为什么进程仍然继续正常运行直到达到内存限制的原因)。我想对象中可能有更多来自父级的 db 句柄,这些句柄没有被子级使用但仍被引用。

我的问题 -

  1. 循环引用是问题的唯一原因,还是有其他问题导致进程使用这么多内存?

  2. 句柄的共享是否会导致内存爆炸(换句话说,共享的 DB 句柄导致 GC 无法释放空间)?

  3. 如果它确实是共享数据库句柄,我想我可以说 $dbHandle = 0 以摆脱引用(如果 $dbHabndle 正在引用该特定句柄)。我说的对吗?

  4. 我正在尝试查看代码以查看在其他地方还有对父数据库句柄的引用(并找到至少一个更多引用)。我还有其他方法可以做到这一点吗?有没有办法打印出一个对象的所有属性?

编辑: 并非所有线程(由于 windows 中的 perl fork 调用)都是同时产生的。它产生最多 n 个线程(其中 n 是可配置的数字)。一旦一个线程完成了它的执行,这个进程就会产生另一个线程。此时 n 设置为 10,但是我已将 n 更改为 1(因此一次只运行一个额外的线程),但我仍然达到了内存限制。

【问题讨论】:

  • 你的代码是什么样的?你使用什么模块?你如何使用它们?需要更多详细信息。
  • @TLP - 我只提到了我认为导致问题的过程的主要部分。它是一个更大系统的一部分,该系统使用大量模块,如 DBD::Oracle、XML::Simple、Net::FTP、File::Path 等。
  • 您在使用数据库句柄后是否断开连接?
  • 循环引用是在 Perl 中造成内存泄漏的主要方式之一——它使用引用计数来检测内存是否可能被释放。循环引用永远不会降为零,因此它永远不会释放。
  • @TLP - 查看代码,我认为现在不会发生这种情况。让我再次检查并修复它,如果确实如此。但是,这会导致内存爆炸吗?一旦线程完成并因此收集垃圾,句柄对象的引用是否应该超出范围。

标签: multithreading perl garbage-collection


【解决方案1】:

edit:事实证明,这并不能解决 Ops 问题。仍然可能对未来的读者有所帮助。

我们对你的情况了解不多,而且你的程序听起来很复杂,对我来说只是 fork 6000 次。但我还是会尝试回答,如果我的假设是错误的,请纠正我。

您似乎在 Windows 上。需要注意的是,Windows 没有fork() 系统调用。正如您特别指出的那样,您“分叉”,我只是假设您实际上使用了该 Perl 命令。在 Windows 上,这将尽可能地模拟fork(),但这基本上意味着,您看到的所有分叉进程实际上只是原始进程中的线程,只是假装是您的进程。为此,他们复制完整的解释器状态。请参阅http://perldoc.perl.org/perlfork.html 了解更多信息。特别是以下部分似乎适用于您:

资源限制

在操作系统看来,通过 fork() 模拟创建的伪进程只是同一进程中的线程。这意味着操作系统强加的任何进程级别限制都适用于所有伪进程。这包括操作系统对打开文件、目录和套接字句柄的数量施加的任何限制、对磁盘空间使用的限制、对内存大小的限制、对 CPU 使用的限制等。

如果你 fork 这么多伪进程,你需要大量内存,因为你还必须经常复制解释器状态。根据您的程序的复杂性及其结构,这很可能是一笔不小的内存。

正如http://msdn.microsoft.com/en-us/library/windows/desktop/aa366778%28v=vs.85%29.aspx 告诉我们的那样,您提到的 1.7GB 与某些 Windows 版本作为单个进程的内存限制强加给您的 2GB 相差不远。

我的疯狂猜测是,实际上你只是通过产生那么多线程来达到这个限制,每个线程都有自己的解释器状态副本和所有内容。

使用一些线程库而不是要求 Perl 为您模拟单个进程可能会好得多。不用说(我希望)你并没有真正通过让 6000 个线程获得任何优势,比如说 16 个。如果你试图让所有线程同时做某事,你实际上很可能会遇到减速,具体取决于如何处理线程。

【讨论】:

  • “你不会因为拥有 6000 个线程而不是 16 个线程而真正获得任何优势”,当然,除非他们的机器有 6000 个内核 ;)
  • 道歉@DeVadder,我没有很好地解释自己。您对 fork 的解释是正确的,并且该过程确实会产生不同的线程。但是它不会同时产生 6000 个线程。它确保在任何时间点最多有 10 个线程。一旦一个线程完成它的工作并死亡,它就会再产生 1 个线程。
  • 让我编辑问题以反映该部分。很抱歉没有首先解释这一点
  • @Harsha 嗯,所以如果你真的 wait 一个接一个地获取你的伪进程但仍然存在内存泄漏,这意味着 fork 仿真无法从伪孩子。在这种情况下,我无法帮助您期望建议使用 Linux(我知道,可能不是一个可行的选择),其中实际的 fork() 的存在将使 GC 是否能够在孩子们收获后清理所有东西无关紧要.当然,除非泄漏是在原始过程中发生的。 ^^
  • @DeVadder 我刚刚编写了一个快速程序,它分叉 10 个线程(每个线程占用一些内存)并等待并获取更多线程以始终保持在 10 个线程。该过程的内存确实增加了(并且几乎保持不变)。所以显然内存泄漏隐藏在代码和原始过程中的某个地方:(我解决了数据库句柄的问题,但这也无济于事。我想我需要做更多的调查
【解决方案2】:

除了已经提供的 cmets,我想强调一下 DeVadder 关于 fork 在 Windows 中的行为的观点,Perl 线程可能是一个更好的解决方案,但您确定 DBD 模块可以安全地供多人使用吗?进程/分叉/线程等而不设置一些额外的参数?

在使用 DBI 模块使用线程模块访问多处理代码中的 SQLite DB 时,我遇到了类似的错误。它通过将 DBI 提供的数据库句柄的“use_immediate_transaction”选项设置为 1 来解决。如果您不熟悉 Perl 线程的工作方式,它们不是线程,它们会创建解释器的副本以及您拥有的所有内容创建时的内存,但即使我在每个“线程”中单独处理数据库,我也会得到“数据库锁定”和各种其他错误。如果没有这些额外选项中的一些,DBD 可能无法在多处理环境中正常运行。

另外,为什么要创建 6000 个分叉,使用 thread::queue 和线程模块,创建一个由几个工人组成的工人池(每个核心一个?)并回收工人。您在每次分叉时都付出了很多开销,却没有任何收获。

【讨论】:

  • 系统已经启动并运行了2年多,所以我想到目前为止我们的DBD模块没有任何重大问题(当然我不能确定它不会以后再制造问题)。也许线程是一个更好的解决方案,但我不确定为什么最初的设计师(不幸的是,他现在没有与我们合作)选择走分叉路。
猜你喜欢
  • 1970-01-01
  • 2014-09-02
  • 1970-01-01
  • 2015-04-27
  • 1970-01-01
  • 1970-01-01
  • 2012-01-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多