How can I resolve inexplicable ObjectDisposedExcep

2019-04-14 20:09发布

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.

3条回答
forever°为你锁心
2楼-- · 2019-04-14 20:31

My ViewModelBase class destructor has a WriteLine to log when the destructor is called,

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.

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).

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.

So that leaves me thinking the CLR is being dumb.

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.

查看更多
甜甜的少女心
3楼-- · 2019-04-14 20:37

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.

查看更多
Juvenile、少年°
4楼-- · 2019-04-14 20:51

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

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.

查看更多
登录 后发表回答