如何在C#/ MVVM应用程序中解决无法解释的ObjectDisposedExceptions?

我编写了我的第一个MVVM应用程序。 当我关闭应用程序时,我常常因ObjectDisposedException而导致崩溃。 应用程序窗口消失后,应用程序即会崩溃。

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

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

我认为发生了什么:

我认为GarbageCollector在处理我的ViewModel时做的很有趣。 我的ViewModelBase类析构函数在调用析构函数时有一个WriteLine来记录,而在我的4个ViewModel中,只有2或3个被处理掉了,它似乎根据结帐而变化(例如,当我在我的运行时,我看到一直在重复顺序,但我的同事看到不同的序列与不同的对象处置)。

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

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

其他可能有用的细节:
我的所有Views和ViewModel都是在App.xaml.cs文件的Application的Startup事件处理程序中创建的。 同一个处理程序将ViewModels分配给DataContexts。 我不确定这是否是正确的MVVM实践(正如我所说的,我的第一个MVVM应用程序),但我不明白为什么它会导致不良行为。

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

您的应用程序抛出exception,因为当您的主应用程序退出时,ViewModel销毁的日志记录操作尚未完成。

您会发现,为了执行实际的文件编写,会生成子进程。 如果在主应用程序退出时尚未完成,那么您将收到错误消息。

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

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

我的ViewModelBase类析构函数有一个WriteLine来记录析构函数,

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

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

在我的4个ViewModel中,只有2个或3个被处理掉,而且它似乎根据结账而有所不同(例如,当我在我的运行时,我看到一致的重复序列,但我的同事看到了不同的序列,并且处理了不同的对象)。

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

正确编写析构函数是C#中最难做的事情之一; 在进程关闭之前,您在最后一轮结束期间获得exception表示您可能做错了。

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

为您的错误指责工具不太可能帮助您解决问题。

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

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

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

  • 如果对象位于锁定目的以确保一致的突变,并且抛出exception,并且finally块不恢复一致状态,则该对象在完成时将处于不一致状态。 由于中止事务导致内部状态不一致的对象,析构函数必须是健壮的。

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

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

  • 因为等待破坏的对象是活着的 ,并且它们被识别为需要破坏,因为GC将它们归类为死亡等待完成的对象在世代垃圾收集器中自动向上移动一代 。 这意味着垃圾收集器回收存储不会发生,直到对象第二次死亡。 由于对象刚刚移动到后一代,因此可能不会长时间确定。 析构函数导致短期内存分配变得更长寿,这在某些情况下会严重影响垃圾收集器的性能。 在为一个大的,短命的对象编写一个析构函数之前要仔细考虑(或者更糟糕的是,你将要生成数百万的小型短期对象); 除非您明确禁止完成,否则无法通过gen零收集器释放具有析构函数的对象

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

  • 抛出未处理exception的析构函数会使进程陷入危险状态。 如果发生这种情况,运行时引擎完全有权在整个过程中失败。 (虽然不需要这样做。) 析构函数绝不能抛出未处理的exception。

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

我认为GarbageCollector在处理我的ViewModel时做的很有趣。 我的ViewModelBase类析构函数有一个WriteLine,用于在调用析构函数时进行记录

那可能是问题所在。 你根本不应该使用终结器,除非你真的有充分的理由这样做,并且记录东西肯定不是其中之一。

您必须了解Finalizer不会以可预测的顺序运行。 GC可以按照它想要的顺序调用终结器,这可能解释了为什么你会得到看似随机的exception行为。