4

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

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

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

如果您能够获得要编译的代码,则可以使用此测试代码来重现问题。


问题

名为 的最新变更集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 方法有关,即使该类仍在使用中。

我可以通过这个文件(下载)1始终如一地实现它。提前调用的终结器位于DBAccessor.cpp文件中的NodeIdCollection类中。如果您能够运行上面链接的代码(由于对 boost 库的依赖,此项目可能难以设置),应用程序将失败并出现异常,因为列表设置为 null 并且指针为由于终结器运行而重置。_nodes_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 语句对我来说是错误的,但是,使用该方法非常有必要防止任何内存泄漏。没有它,即使调用了 pst 类的 dispose 方法,也永远不会调用 dispose 并且永远不会释放内存。

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

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

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


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

谢谢!


1此文件是可免费获得的 PST 文件安然数据集的一部分

4

2 回答 2

2

clr_scoped_ptr是我的,来自这里

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

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

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

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


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

于 2011-10-15T22:57:22.063 回答
-1

我认为,从一些Microsoft 文档中刷新我对析构函数/finalalisers 的记忆,你至少可以稍微简化你的代码。

这是我的序列版本:

DBContext::~DBContext()
{
    this->!DBContext();
}

DBContext::!DBContext()
{
    delete _pst;
    _pst = NULL;
}

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

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

希望这有所帮助,

于 2011-10-15T19:43:22.980 回答