Sunday, August 15, 2010

Event De-registration and Memory Leaks

15-August-2010

I think its pretty safe to say we would all recommend deregistering/unsubscribing events when we are finished with them.  I know I was told when learning about events years ago, that if you do not, memory leaks would result.  After looking into the GC recently I thought it might be interesting to get some specifics around this. 

So, what happens when you don't unsubscribe from events and just pray the GC can figure it out?  Unsurprisingly some objects stay alive a lot longer than they should.  

The problem can be summarised as follows:
Events work by giving a delegate to the Source object for it to call when an event occurs.  The delegate is tightly bound with a strong reference to the listener. Most of the time this works fine.  A sloppy developer might never unsubscribe the events they register for, this isn't good but its not necessarily catastrophic.  The listener object cannot be garbage collected until the source object is also a candidate for collection (again assuming no event unsubscription otherwise it would be).  Eventually the source object will be garbage collected and then so will the Listener.  Not timely release of memory but at least not technically a memory leak. 

A memory leak, at least IMHO, is when objects are never removed from memory and the memory is not reclaimed until the process is terminated. So not technically a memory leak right?  Well it can be if your source object is a singleton or static effectively making it immune to garbage collection along with any object who subscribed to its events. Fortunately, in .NET memory leaks are far less frequent than in unmanaged code.

Lets take a look at a demo of good and bad event handling.
The application is made up of a Shell Controller and up to four child panel controllers.  The Shell Controller is obviously going to be long lived even though it is not static, it is controlling the root of the application and therefore will not garbage collected until the process closes. Clicking "Add" will add a new Panel to the shell controller and the UI.  When the Panel is added, it subscribes to the Shell Controller's Ping event.

The Panel starts to receive the Ping event from the Shell Controller and displays the event as text in a list box.
When the "X" buttons are clicked on each panel the panels are removed from the UI and all references to them are destroyed.  This effectively means they should be candidate for collection right? No, because I did not unsubscribe to the Ping event for each Panel.  Some developers think by adding code to the Finalizer (destructor) it will deregister the event when the GC collects it.  No, wrong again. The GC calls the Finalizer and the object isn't a candidate for collection until the event is unsubscribed.

As you can see in the above image the number of alive objects are currently 3.  This is incremented when a new Panel is created and is only decremented when a Panel Finalizer is called, meaning the panel is being collected.

Here I have removed two panels and clicked the button "ForceGC" to force an immediate garbage collection.  As you can see the Alive objects still reads 3.  Meaning no Finalizer has been called to decrement the Alive Objects counter.

The test application runs in two modes "Sloppy mode" or "Best Practice mode"  this is designated by the toggle button.  When the toggle button is depressed in, the application will unsubscribe from the Ping event when a panel is closed.
3 Panels created, the counter reads 3.  Lets close 2.

After removing 2, and clicking force GC, the counter immediately changes to 1. Excellent.  Incidentally, if you wait 1-2 minutes it will collect by itself without having to force a collection.

Hope this clarifies events best practice, with good evidence.

More information:
There are some handy new classes in .Net 4 that allow weak events. See http://msdn.microsoft.com/en-us/library/aa970850.aspx.  This is definitely my recommendation for use with WPF and an Event Aggregator, Attached Property, or Decorator, since these are normally implemented as statics or singletons. Ie, short lived listener subscribing to long lived singleton equals tight-rope-walk.

No comments:

Post a Comment