2

我已经编写了我的第一个 MVVM 应用程序。当我关闭应用程序时,我经常遇到由 ObjectDisposedException 引起的崩溃。在应用程序窗口消失后,应用程序死亡时出现崩溃。

获取堆栈跟踪很困难(请参阅我的另一个问题),但最后我做到了,发现我的堆栈跟踪完全包含在 C# 库中(kernel32!BaseThreadStart、mscorwks!Thread、mscorwks!WKS 等)。

此外,这种崩溃是不一致的。在我最后一次结帐和重建之后,它停止了......一段时间。然后它回来了。一旦它开始发生,它就会继续发生,即使我“清理”并重建。但是擦除和结帐有时会使其停止一段时间。

我认为正在发生的事情:

我认为 GarbageCollector 在处理我的 ViewModel 时正在做一些有趣的事情。我的 ViewModelBase 类析构函数在调用析构函数时有一个 WriteLine 来记录,在我的 4 个 ViewModel 中,只有 2 或 3 个被释放,并且它似乎因结帐而异(例如,当我在我的上运行它时,我看到一个持续重复序列,但我的同事看到了一个不同的序列,其中放置了不同的对象)。

由于堆栈跟踪中没有我的代码调用,我认为这意味着调用已处置对象的方法的不是我的代码。所以这让我觉得CLR是愚蠢的。

这有意义吗?有什么方法可以让 GC 保持一致吗?这是红鲱鱼吗?

其他可能有帮助的详细信息:
我的所有视图和视图模型都是在我的 App.xaml.cs 文件的应用程序的启动事件处理程序中创建的。同一个处理程序将 ViewModels 分配给 DataContexts。我不确定这是否是正确的 MVVM 做法(正如我所说,我的第一个 MVVM 应用程序),但我不明白为什么它会导致不良行为。

如有必要,我可以粘贴代码。

4

3 回答 3

13

我的 ViewModelBase 类析构函数有一个 WriteLine 在调用析构函数时记录,

这真的很糟糕。我希望你只在调试版本中启用它。

你绝对不应该在析构函数中做任何复杂的事情,比如创建文件句柄、操作磁盘状态等等。那只是在自找最坏的麻烦。析构函数应该清理非托管资源并且绝对不做任何其他事情。

在我的 4 个 ViewModel 中,只有 2 个或 3 个被处理掉,而且它似乎因结帐而异(例如,当我在我的设备上运行它时,我看到了一个一致重复的序列,但我的同事看到了一个不同的序列,其中处理了不同的对象)。

正如我们将在下面看到的那样,您会看到事情在不同的时间以不同的顺序发生是完全可以预料的。

正确编写析构函数是 C# 中最难的事情之一。如果您在进程关闭之前的最后一轮最终确定中遇到异常,则表明您可能做错了。

所以这让我觉得CLR是愚蠢的。

将错误归咎于工具不太可能帮助您解决问题。

在编写任何析构函数之前,每个人都应该知道的事情是:

  • 析构函数不一定与任何其他代码在同一线程上运行。这意味着您可能会遇到竞争条件、锁排序问题、由于内存模型薄弱而导致读写及时移动等等。如果您使用析构函数,您将自动编写多线程程序,因此您必须设计程序以防御所有可能的线程问题。那是你的责任,而不是 CLR 的责任。如果您不愿意承担编写线程安全对象的责任,请不要编写析构函数。

  • 即使对象从未初始化,析构函数也会运行。完全有可能在分配对象并且代码在构造函数中途运行之后,抛出异常。该对象已分配,您没有抑制终结,因此必须将其销毁。面对未完全初始化的对象,析构函数必须是健壮的。

  • 如果一个对象处于一个旨在确保一致突变的锁下,并且抛出了异常,并且 finally 块没有恢复一致状态,那么该对象在 finalized 时将处于不一致状态。面对由于事务中止而导致内部状态不一致的对象,析构函数必须是健壮的。

  • 析构函数可以按任何顺序运行。如果你有一棵相互引用的对象树,它们同时都死了,那么每个对象的析构函数都可以随时运行。析构函数在面对其内部状态引用其他已经或尚未被破坏的对象的对象时必须是健壮的。

  • 根据垃圾收集器,在终结器队列上等待销毁的对象是活动的。析构函数使之前死掉的对象暂时(我们希望!)再次活跃起来。如果您的程序逻辑依赖于死对象保持死状态,那么您必须非常小心您的析构函数。(如果析构逻辑使对象再次永久活跃,您可能会遇到大问题。不要那样做。)

  • 因为等待销毁的对象是活动的,并且它们被识别为需要销毁,因为 GC 将它们分类为死亡所以等待终结的对象在分代垃圾收集器中自动向上移动一代。这意味着在对象第二次死亡之前,垃圾收集器无法回收存储。由于该对象只是移到了后代,因此可能需要很长时间才能确定。析构函数会导致短暂的内存分配变得更长寿,这在某些情况下会严重影响垃圾收集器的性能。在为一个大的、短命的对象(或者更糟的是,一个你将要制造数百万个短命的小对象)编写析构函数之前,请仔细考虑;除非您明确禁止 finalization ,否则零代收集器无法释放具有析构函数的对象

  • 不保证调用析构函数。垃圾收集器不需要在进程关闭之前运行对象的析构函数,即使已知它们已死。您的逻辑不能依赖于调用析构函数的正确性。很多事情都可以阻止调用析构函数——例如,FailFast,堆栈溢出异常,或者有人从墙上拔出电源线。面对从未调用过的析构函数,程序必须是健壮的。

  • 抛出未处理异常的析构函数使进程进入危险状态。如果发生这种情况,运行时引擎完全有权对整个过程进行故障处理。(尽管不是必须这样做。)析构函数决不能抛出未处理的异常。

如果您不愿意忍受这些限制,那么首先不要编写析构函数。无论你喜不喜欢,这些限制都不会消失。

于 2011-10-19T16:33:35.560 回答
4

您的应用程序正在引发异常,因为当您的主应用程序退出时,您对 ViewModel 销毁的日志记录操作尚未完成。

您会发现,为了执行实际的文件写入,产生了一个子进程。如果在您的主应用程序退出时这还没有完成,那么您将收到一个错误。

如果您要执行此类操作,那么您需要您的主应用程序等待一段时间,以便任何子进程/线程池线程等在退出之前完成。

如果您希望确保可以记录应用程序关闭期间发生的事件,那么我建议您将日志记录过程(实际写入日志文件)作为您发布消息的单独主线程运行。这样,您的应用程序可以在您的日志记录过程完成写入磁盘之前关闭。

于 2011-10-19T16:32:56.940 回答
2

我认为 GarbageCollector 在处理我的 ViewModel 时正在做一些有趣的事情。我的 ViewModelBase 类析构函数有一个 WriteLine 在调用析构函数时记录

这可能就是那里的问题。除非您真的有充分的理由这样做,否则您根本不应该使用终结器,而日志记录绝对不是其中之一。

您必须了解终结器不会以可预测的顺序运行。GC 可以在它想要的时间和顺序中调用终结器,这可能解释了为什么你会得到看似随机的异常行为。

于 2011-10-19T16:34:05.090 回答