C++/CLI 库中的早期终结和内存泄漏

Posted

技术标签:

【中文标题】C++/CLI 库中的早期终结和内存泄漏【英文标题】:Early finalization and memory leaks in C++/CLI library 【发布时间】:2011-12-07 05:11:31 【问题描述】:

我遇到了在我正在处理的 C++/CLI(和 C#)项目中似乎在早期调用终结器的问题。这似乎是一个非常复杂的问题,我将从代码中提到很多不同的类和类型。幸运的是,它是开源的,您可以在这里关注:Pstsdk.Net(mercurial 存储库)我还尝试在适当的地方直接链接到文件浏览器,这样您就可以在阅读时查看代码。我们处理的大部分代码都在存储库的pstsdk.mcpp 文件夹中。

现在的代码处于相当可怕的状态(我正在处理它),而我正在处理的代码的当前版本位于Finalization fixes (UNSTABLE!) 分支中。该分支中有两个变更集,为了理解我冗长的问题,我们需要同时处理这两个。 (变更集:ee6a002df36f 和 a12e9f5ea9fe)

对于某些背景,这个项目是用 C++ 编写的 unmanaged library 的 C++/CLI 包装器。我不是该项目的协调员,有几个设计决策我不同意,我相信很多看代码的人都会同意,但我离题了。我们将大部分原始库的层封装在 C++/CLI dll 中,但在 C# dll 中公开易于使用的 API。这样做是因为该项目的目的是将整个库转换为托管 C# 代码。

如果您能够获得要编译的代码,您可以使用this test code 重现问题。


问题

名为moved resource management code to finalizers, to show bug 的最新变更集显示了我遇到的原始问题。此代码中的每个类都使用相同的模式来释放非托管资源。这是一个示例(C++/CLI):

DBContext::~DBContext()

    this->!DBContext();
    GC::SuppressFinalize(this);


DBContext::!DBContext()

    if(_pst.get() != nullptr)
        _pst.reset();            // _pst is a clr_scoped_ptr (managed type)
                                 // that wraps a shared_ptr<T>.

此代码有两个好处。首先,当这样的类在using 语句中时,资源会立即被正确释放。其次,如果用户忘记了一个 dispose,当 GC 最终决定终结该类时,非托管资源将被释放。

这是这种方法的问题,我根本无法理解,有时,GC 会决定最终确定一些用于枚举文件中数据的类。许多不同的 PST 文件都会发生这种情况,我已经能够确定它与被调用的 Finalize 方法有关,即使该类仍在使用中。

我可以通过this file (download)1 始终如一地实现它。提前调用的终结器位于DBAccessor.cpp 文件中的NodeIdCollection 类中。如果您能够运行上面链接的代码(由于对 boost 库的依赖,此项目可能难以设置),应用程序将失败并出现异常,因为 _nodes 列表设置为 null 并且_db_ 指针由于终结器运行而被重置。

1) NodeIdCollection 类中的枚举代码是否有任何明显的问题会导致 GC 在该类仍在使用时完成它?

我只能通过下面描述的解决方法让代码正常运行。


一个难看的解决方法

现在,我可以通过将所有资源管理代码从每个终结器 (!classname) 移动到析构函数 (~classname) 来解决这个问题。这已经解决了这个问题,虽然它并没有解决我对为什么这些类提前完成的好奇心。

但是,这种方法存在问题,我承认这更多的是设计问题。由于在代码中大量使用了指针,几乎每个类都处理自己的资源,并要求处理每个类。这使得使用枚举非常难看(C#):

   foreach (var msg in pst.Messages)
   
      // If this using statement were removed, we would have
      // memory leaks
      using (msg)  
      
             // code here
      
   

作用于集合中项目的 using 语句对我来说是错误的,但是,使用这种方法来防止任何内存泄漏是非常必要的。没有它,dispose 永远不会被调用并且内存永远不会被释放,即使 pst 类的 dispose 方法被调用。

我有意改变这种设计。最初编写此代码时的基本问题,除了我对 C++/CLI 知之甚少甚至一无所知的事实之外,我无法将本机类放入托管类中。我觉得可以使用作用域指针,当类不再使用时会自动释放内存,但我不能确定这是否是一种有效的方法,或者它是否可以工作。所以,我的第二个问题是:

2) 以轻松的方式处理托管类中的非托管资源的最佳方法是什么?

详细来说,我可以用最近添加到代码中的clr_scoped_ptr 包装器替换本机指针吗(clr_scoped_ptr.h 来自this stackexchange 问题)。或者我是否需要将本机指针包装在 scoped_ptr&lt;T&gt;smart_ptr&lt;T&gt; 之类的东西中?


感谢您阅读所有这些,我知道这很多。我希望我已经足够清楚,以便我可以从比我更有经验的人那里获得一些见解。这是一个很大的问题,我打算在允许的情况下增加赏金。希望有人可以提供帮助。

谢谢!


1此文件是免费提供的 enron dataset PST 文件的一部分

【问题讨论】:

我严重怀疑 .NET 终结线程是否正在调用终结器如果对象仍在使用中。你能把代码缩小到一个非常简单的例子来展示这种行为吗? @LasseV.Karlsen - 我当然可以尝试,但我不确定由于包装代码大量使用了 boost 库,它会有多简单,我想我可能必须包含它以及让这个问题重现。不过我会尽力而为。 @LasseV.Karlsen - 我正在努力重现它(到目前为止我一直没有成功),但我想解决一件事。上面的代码将表明当对象仍在使用时确实会发生终结。当我枚举它时,我可以在集合的终结器中放置一个断点。大约进行到一半,还有更多工作要做,终结器中的断点被击中。有趣的是我仍然可以访问该对象,但是随着终结器的运行,内部对象会根据我的代码被删除。我会期待一个 ObjectDisposedException? 您似乎侵犯了我的版权,因为您没有遵守我的(非常慷慨的)许可条件。这可以通过编辑pstsdknet.codeplex.com/SourceControl/changeset/view/… 的版权声明来解决 @BenVoigt - 我将添加它。我确保版权保留在源代码中,但我忽略了为二进制文件执行此操作。它在一个新的变更集中。 【参考方案1】:

clr_scoped_ptr 是我的,来自here。

如果有任何错误,请告诉我。

即使我的代码并不完美,使用智能指针也是处理此问题的正确方法,即使在托管代码中也是如此。

您不需要(也不应该)在终结器中重置 clr_scoped_ptr。每个clr_scoped_ptr 本身都将由运行时完成。

使用智能指针时,您不需要编写自己的析构函数或终结器。编译器生成的析构函数会自动调用所有子对象的析构函数,每个子对象终结器都会在被回收时运行。


仔细查看您的代码,NodeIdCollection 中确实存在错误。 GetEnumerator() 每次调用时都必须返回一个不同的枚举器对象,以便每个枚举都从序列的开头开始。您正在重用单个枚举器,这意味着该位置在对GetEnumerator() 的连续调用之间共享。这很糟糕。

【讨论】:

@BenVoight - 不知道否决票,但谢谢!这确实是枚举器的问题。 NodeIdCollection 共享枚举器,超出范围并被 GC 收集,并删除了枚举器,即使我仍在使用它。出于某种原因,我曾期望 ^enumerator 引用可以防止这种情况发生,但正如您所指出的那样,这样做是不合适的。 @BenVoight - 我已经决定代替不赞成票,您花时间查看代码以提供帮助,以及您的 clr_scoped_ptr 运行良好的事实,我想给您额外的 100 次代表,我会在 24 小时内完成。 @ChristopherCurrens:别出汗。没有给出理由就放弃投票的人不值得担心。我只是留下评论,希望选民能过来解释一下。【参考方案2】:

刷新我对析构函数/finalalisers 的记忆,从一些Microsoft documentation,你至少可以简化你的代码,我想。

这是我的序列版本:

DBContext::~DBContext()

    this->!DBContext();


DBContext::!DBContext()

    delete _pst;
    _pst = NULL;

“GC::SupressFinalize”由 C++/CLI 自动完成,因此无需这样做。由于 _pst 变量是在构造函数中初始化的(并且删除 null 变量无论如何都不会导致任何问题),我看不出有任何理由使用智能指针使代码复杂化。

在调试说明中,我想知道您是否可以通过调用“GC::Collect”来帮助使问题更加明显。这应该会强制为您确定悬空对象。

希望对你有所帮助,

【讨论】:

我不知道 clr_scope_ptr 是什么。但是当客户端代码多次调用 Dispose() 时,此代码很容易多次删除同一个对象。没有帮助。 好点,编辑有帮助吗?我最初的帖子是基于 clr_scope_ptr 的不确定性并试图将其删除... 不是真的,它不能编译。使用 nullptr。 嗯,我假设 _pst 是一个指向 unmanaged 资源的指针。您认为它是托管资源吗? 不,我确定它是不受管理的。代码是用 /clr 编译的。这禁止 NULL,过早地引导 C++11 6 年。使用 0 或 nullptr。最好先编译你自己发布的代码 :)

以上是关于C++/CLI 库中的早期终结和内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

PerfView专题 (第十篇):洞察 C# 终结队列引发的内存泄漏

C语言中的指针和内存泄漏

如何解决 xcode(仪器)中的 iPhone 应用程序内存泄漏

常见的 Perl 内存/引用泄漏模式?

转C 语言中的指针和内存泄漏

使用仪器检查 xcode 4 中的内存泄漏