在我所从事的c++/CLI(和c#)项目中,我遇到了终结器似乎在早期被调用的问题。这似乎是一个非常复杂的问题,我将从代码中提到很多不同的类和类型。幸运的是,它是开源的,您可以在这里跟踪:Pstsdk。我还尝试在适当的地方直接链接到文件浏览器,这样您就可以在阅读时查看代码。我们处理的大部分代码都在存储库的pstsdk.mcpp
文件夹中。
代码现在是在一个相当可怕的状态(我正在工作),我正在工作的代码的当前版本是在Finalization fixes (UNSTABLE!)
分支。在这个分支中有两个变更集,为了理解我这个冗长的问题,我们需要处理这两个。(changessets: e6a002df36f and a12e9f5ea9fe)
对于一些背景,这个项目是一个用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库的依赖,这个项目可能很难设置),应用程序将异常失败,因为_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
包装器(clr_scoped_ptr.h)替换本机指针吗?或者我需要将本机指针包装成scoped_ptr<T>
或smart_ptr<T>
之类的东西吗?
感谢您阅读所有这些,我知道这是很多。我希望我已经说得够清楚了,这样我就可以从比我更有经验的人那里得到一些见解。这是一个很大的问题,我打算在它允许我的时候再加一个赏金。希望有人能帮忙。
谢谢!
1此文件是免费提供的安然PST文件数据集的一部分
clr_scoped_ptr
是我的,来自这里。
如果有任何错误,请告诉我。
即使我的代码不完美,使用智能指针是处理这个问题的正确方法,即使是在托管代码中。
你不需要(也不应该)在终结器中重置clr_scoped_ptr
。每个clr_scoped_ptr
本身将由运行时完成。
使用智能指针时,不需要编写自己的析构函数或终结器。编译器生成的析构函数将自动调用所有子对象的析构函数,并且每个子对象的终结器将在收集时运行。
仔细看你的代码,确实有一个错误在NodeIdCollection
。GetEnumerator()
每次被调用时必须返回一个不同的枚举对象,以便每个枚举都从序列的开头开始。您正在重用单个枚举器,这意味着在对GetEnumerator()
的连续调用之间共享该位置。这是不好的。
从一些微软文档中刷新了我对析构函数/终结器的记忆,我认为你至少可以稍微简化一下你的代码。
这是我对你的序列的版本:
DBContext::~DBContext()
{
this->!DBContext();
}
DBContext::!DBContext()
{
delete _pst;
_pst = NULL;
}
"GC: SupressFinalize"是由c++/CLI自动完成的,所以不需要。由于_pst变量是在构造函数中初始化的(并且删除null变量不会导致任何问题),我认为没有任何理由通过使用智能指针来使代码复杂化。
在调试笔记中,我想知道您是否可以通过对"GC::Collect"进行一些调用来帮助使问题更明显。这将为您强制完成悬空对象。
希望这对你有帮助,