如何解决 C# / MVVM 应用程序中无法解释的 ObjectDisposeExceptions?
我已经编写了我的第一个 MVVM 应用程序。当我关闭应用程序时,经常会因 ObjectDisposeException 导致崩溃。当应用程序终止时,应用程序窗口消失后,崩溃就会出现。
获取堆栈跟踪很困难(查看我的其他问题),但最终我做到了,并发现我的堆栈跟踪完全包含在 C# 库中(kernel32!BaseThreadStart、mscorwks!Thread、 mscorwks!WKS 等)。
此外,这次崩溃是不一致的。在我上次检查和重建之后,它停止发生了......有一段时间。然后它又回来了。一旦它开始发生,它就会一直发生,即使我“清理”并重建。但擦拭和结帐有时会使其停止一段时间。
我认为正在发生的事情:
我认为 GarbageCollector 在处理我的 ViewModel 时做了一些有趣的事情。我的 ViewModelBase 类析构函数有一个 WriteLine 来记录析构函数被调用时的情况,并且在我的 4 个 ViewModel 中,只有 2 或 3 个被释放,并且它似乎根据结帐而变化(例如,当我在我的析构函数上运行它时,我看到始终重复的序列,但我的同事看到了不同的序列,并放置了不同的对象)。
由于堆栈跟踪中没有我的代码调用,我认为这意味着不是我的代码调用已处理对象的方法。所以这让我觉得 CLR 很愚蠢。
这有道理吗?有什么办法可以让 GC 保持一致吗?这是红鲱鱼吗?
其他可能有帮助的详细信息:
我的所有视图和 ViewModel 都是在 App.xaml.cs 文件的应用程序的启动事件处理程序中创建的。同一个处理程序将 ViewModel 分配给 DataContext。我不确定这是否是正确的 MVVM 实践(正如我所说,我的第一个 MVVM 应用程序),但我不明白为什么它会导致不良行为。
如果需要的话我可以粘贴代码。
I've written my first MVVM application. When I close the application, I often get a crash cause by an ObjectDisposedException. The crash appears as the application dies, just after the app window disappears.
Getting a stacktrace was difficult (see my other question), but finally I did, and found that my stacktrace is entirely contained within C# libraries (kernel32!BaseThreadStart, mscorwks!Thread, mscorwks!WKS, etc).
Furthermore, this crash is inconsistent. After my last checkout and rebuild, it stopped happening... for a little while. Then it came back. Once it starts happening, it keeps happening, even if I "Clean" and rebuild. But a wipe-and-checkout sometimes makes it stop for a while.
WHAT I THINK IS HAPPENING:
I think the GarbageCollector is doing something funny when disposing my ViewModels. My ViewModelBase class destructor has a WriteLine to log when the destructor is called, and of my 4 ViewModels, only 2 or 3 get disposed, and it seems to vary depending on checkout (e.g. when I run it on mine, I see a consistently repeated sequence, but my colleague sees a different sequence with different objects disposed).
Since the stacktrace has none of my code's calls in it, I think that means that it's not my code that's calling a disposed object's method. So that leaves me thinking the CLR is being dumb.
Does this make sense? Is there some way I can get the GC to be consistent? Is this a red herring?
Other details that might help:
All of my Views and ViewModels are created in my App.xaml.cs file's Application's Startup event handler. That same handler assigns ViewModels to DataContexts. I'm not sure if this is correct MVVM practice (as I said, my first MVVM app), but I don't see why it would cause bad behavior.
I can paste code if necessary.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
这真的很糟糕。我希望您只在调试版本中启用它。
您绝对不应该在析构函数中执行任何复杂的操作,例如创建文件句柄、操作磁盘状态等。这无异于自找最糟糕的麻烦。 析构函数应该清理非托管资源,并且绝对不执行任何其他操作。
您看到事情在不同时间以不同顺序发生是完全可以预料的,正如我们将在下面看到的。
正确编写析构函数是 C# 中最难的事情之一;如果您在进程关闭之前的最后一轮完成过程中遇到异常,则表明您可能做错了。
将错误归咎于工具不太可能帮助您解决问题。
在编写任何析构函数之前,每个人都应该了解的事情是:
析构函数不一定与任何其他代码在同一线程上运行。这意味着您可能会遇到竞争条件、锁定顺序问题、由于内存模型较弱而导致读写时间移动等等。 如果您使用析构函数,您将自动编写多线程程序,因此您必须设计程序以防御所有可能的线程问题。这是您的责任,而不是责任CLR 的。如果您不愿意承担编写线程安全对象的责任,那么不要编写析构函数。
即使对象从未初始化,析构函数也会运行。分配对象并且代码在构造函数执行到一半后,完全有可能抛出异常。对象已分配,您没有抑制终结,因此必须将其销毁。 析构函数需要在面对未完全初始化的对象时具有鲁棒性。
如果一个对象处于旨在确保一致突变的锁下,并且抛出异常,则finally块不恢复一致状态,那么对象在终结时将处于不一致状态。 析构函数在面对因事务中止而导致内部状态不一致的对象时需要具有鲁棒性。
析构函数可以按任何顺序运行。如果您有一个对象树,所有对象都互相引用,并且同时死亡,则每个对象的析构函数可以随时运行。 面对内部状态引用已或尚未被破坏的其他对象的对象,析构函数必须具有鲁棒性。
在终结器队列中等待破坏的对象还活着 根据垃圾收集器。 析构函数会导致先前死亡的对象暂时(我们希望!)再次活动。如果您的程序逻辑依赖于死亡对象保持死亡状态,那么您必须非常小心您的析构函数。 (如果析构函数逻辑使对象再次永久存活,那么您可能会遇到大问题。不要这样做。)
因为等待销毁的对象活着,并且它们是被识别为需要销毁,因为 GC 将它们分类为“死亡”,等待终结的对象会在分代垃圾收集器中自动向上移动一代。这意味着垃圾收集器无法回收存储,直到对象第二次死亡为止。由于该对象刚刚转移到下一代,因此可能在很长一段时间内都无法确定。 析构函数会导致短期内存分配变得更长,这在某些情况下会严重影响垃圾收集器的性能。在为大型短期内存分配析构函数之前要仔细考虑对象(或者更糟糕的是,一个寿命较短的小对象,但您将制造数百万个对象); 具有析构函数的对象无法被第 0 代收集器释放,除非您显式抑制终结。
不保证会调用析构函数。垃圾收集器不需要在进程关闭之前运行对象的析构函数,即使已知它们是死的。您的逻辑不能依赖其正确性取决于调用的析构函数。很多事情都可以阻止析构函数被调用——例如,FailFast,或者堆栈溢出异常,或者有人从墙上拔掉电源线。 程序在面对从未被调用的析构函数时必须保持稳健。
抛出未处理异常的析构函数会使进程陷入危险状态。如果发生这种情况,运行时引擎完全有权对整个过程进行故障快速处理。 (尽管不要求这样做。) 析构函数绝不能抛出未处理的异常。
如果您不愿意接受这些限制,那么不要首先编写一个析构函数。无论您是否喜欢,这些限制都不会消失。
That's really bad. I hope you only have that enabled in the debug build.
You absolutely should not ever do anything complicated in a destructor, like creating file handles, manipulating the state of disks, and so on. That is just asking for trouble of the worst possible kind. A destructor should clean up an unmanaged resource and do absolutely nothing else.
That you are seeing things happen in different orders at different times is entirely to be expected, as we'll see below.
Writing a destructor correctly is one of the hardest things to do in C#; that you are getting an exception during the last round of finalization before the process shuts down indicates that you are probably doing it wrong.
Blaming the tool for your error is unlikely to help you fix your problem.
Things everyone should know before writing any destructor are:
Destructors do not necessarily run on the same thread as any other code. That means that you might have race conditions, lock ordering problems, reads and writes moving around in time due to weak memory models, and so on. If you use destructors you are automatically writing a multithreaded program and therefore you have to design the program to be defend against all possible threading issues. That is your responsibility, not the responsibility of the CLR. If you're unwilling to take on the responsibility of writing a threadsafe object then don't write a destructor.
Destructors run even if the object was never initialized. It is perfectly possible that after an object is allocated and code is halfway through a constructor, an exception is thrown. The object is allocated, you did not suppress finalization, and therefore it must be destructed. A destructor is required to be robust in the face of an incompletely initialized object.
If an object is under a lock intended to ensure a consistent mutation, and an exception is thrown, and the finally block does not restore the consistent state, then the object will be in an inconsistent state when finalized. Destructors are required to be robust in the face of objects with inconsistent internal state as a result of aborted transactions.
Destructors can run in any order. If you have a tree of objects that all refer to each other, that are all dead at the same time, the destructors for each object can be run at any time. Destructors must be robust in the face of objects whose internal state refers to other objects that have or have not just been destructed.
Objects awaiting destruction on the finalizer queue are alive according to the garbage collector. A destructor causes a previously dead object to temporarily (we hope!) become alive again. If your program logic depends on dead objects staying dead, you've got to be very careful with your destructors. (And if the destructor logic makes the object permanently alive again, you might have a big problem on your hands. Don't do that.)
Because objects awaiting destruction are alive, and they are identified as needing destruction because the GC classified them as dead, an object awaiting finalization is automatically moved up one generation in the generational garbage collector. This means that the reclamation of the storage by the garbage collector cannot happen until the object is dead for the second time. Since the object just moved to a later generation, that might not be determined for a long time hence. Destructors cause short-lived memory allocations to become much longer-lived, which can seriously impact the performance of the garbage collector in some scenarios. Think very carefully before you write a destructor for a large, short-lived object (or worse, a small short-lived object that you're going to make millions of); objects with destructors cannot be freed by the gen zero collector unless you explicitly suppress finalization.
Destructors are not guaranteed to be called. The garbage collector is not required to run destructors of an object before the process shuts down, even if they are known to be dead. Your logic cannot depend for its correctness on destructors being called. Lots of things can prevent destructors from being called -- a FailFast, for example, or a stack overflow exception, or someone pulling the power cord out from the wall. Programs are required to be robust in the face of destructors never being called.
Destructors that throw unhandled exceptions put the process into a perilous state. The runtime engine is entirely within its rights to failfast the whole process if this happens. (Though it is not required to do so.) A destructor must never throw an unhandled exception.
If you're unwilling to live with these restrictions then do not write a destructor in the first place. Those restrictions are not going away, whether you like them or not.
您的应用程序抛出异常,因为当您的主应用程序退出时,您对 ViewModel 销毁的日志记录操作尚未完成。
您会发现为了执行实际的文件写入,会生成一个子进程。如果在主应用程序退出时尚未完成,那么您将收到错误。
如果您要执行此类操作,那么您需要主应用程序等待一段时间,让任何子进程/线程池线程等完成后再退出。
如果您希望确保可以记录应用程序关闭期间发生的事件,那么我建议您将日志记录进程(实际写入日志文件)作为您发布消息的单独主线程来运行。这样,您的应用程序就可以在日志记录进程完成写入磁盘之前关闭。
Your application is throwing an exception because your logging action on ViewModel destruction hasn't completed when your main application exits.
You'll find that in order to perform the actual file writing a child process is spawned. If this hasn't completed by the time your main application has exited then you'll get an error.
If you're going to perform this type of action then you need your main application to wait for a period for any child processes/threadpool threads etc. to complete before it exits.
If you wish to ensure that you can log events that occur during your application closure then I would suggest that you run your logging process (the actual writing to your log file) as a separate primary thread that you post messages to. That way your application can close before your logging process has completed writing to disk.
这可能就是问题所在。你根本不应该使用终结器,除非你确实有充分的理由这样做,而日志记录绝对不是其中之一。
您必须了解终结器不会按可预测的顺序运行。 GC 可以按照它想要的时间和顺序调用终结器,这可能解释了为什么您会遇到看似随机的异常行为。
That is probably the problem right there. You should not be using finalizers at all unless you really have a good reason to do so, and logging stuff is definitely not one of them.
You have to understand that Finalizers do not run in a predictable order. The GC can call the finalizers when and in the order it wants which probably explains why you are getting seemingly random exception behavior.