Preventing Event-based Memory Leaks – WeakEventManager
Events are a common source of memory leaks in .NET applications.
When an object (the receiver) subscribes to an event on another object (the sender), the sender holds a reference to the receiving object. This is because the standard event mechanism in .NET works based off delegates, which contain two member fields – _target and _methodPtr. In this case, _target acts as a strong reference to the receiver. Later, if the receiver is no longer needed, it must unsubscribe from the event, or the event itself will keep the receiving object rooted, preventing the garbage collector from reclaiming its memory. This can lead to large memory leaks – especially if the receiver itself holds large amounts of memory.
Normally, this isn’t a problem; its easy to just unsubscribe from the event, which prevents the above from occurring. However, there are times when the useful lifetime of a receiver of an event isn’t tied to the sender at all, which can make unsubscribing very difficult. WPF introduced a new class and pattern for handing this situation: the WeakEvent Pattern.
Although there are many good articles describing how to implement the WeakEvent Pattern in WPF, very few articles describe the mechanism of how and why this works.
The issue is in how delegates work. Events are based on delegates, so when a receiver subscribes to a sender’s event, the sender implicitly holds a strong reference to the receiver in it’s delegate’s _target field:
In this case, the sender holds a reference, and if the receiver goes out of scope, or even if it’s disposed via IDisposable, it will never get garbage collected. As long as sender exists, the receiver will exist. The only way to prevent this is to unsubscribe from the event.
The WeakEvent Pattern works around this by changing two things in the chain. First, it uses an intermediary – a middle man class. Instead of the sender attaching to the receiver, it attaches and holds a strong reference to the intermediary class. This class is a class derived from WeakEventManager, and is effectively a Singleton returned by the GetCurrentManager(…) method. Second, the intermediary class holds a weak reference to the receiver instead of a strong reference, and delivers the event through a custom interface (IWeakEventListener) instead of using a delegate:
Now, if the receiver is allowed to go out of scope, nothing in this pattern holds a strong reference to the receiver, and it becomes eligible to be reclaimed by the garbage collector.
This freedom comes with definite cost. First, there is a lot more than a simple delegate call involved in the sender raising an event. Now, when the sender raises the event, the WeakEventManager receives it, and needs to translate it into a new method call, and pass it onto the appropriate receiver. There are quite a few costs involved here, including a significant loss of usability. No longer is our event type safe – it’s always mapped from an EventHandler or EventHandler<T> into a single method on the receiver:
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { }
This is not nearly as nice – since we now have to check the manager type, cast the EventArgs to the appropriate type of EventArgs, and call through to our appropriate delegate. The more types of events we receive, the larger and uglier this method becomes. If you add appropriate error checking, it definitely becomes much less friendly than a simple standard event subscription.
In addition to the receiver needing extra work, the WeakEventManager needs to be customized for each type of event. WPF provides some very useful event managers, including PropertyChangedEventManager and CollectionChangedEventManager, but they are mostly geared towards data binding. If you want to handle your own event, or one not provided, you’ll need to implement your own WeakEventManager following the pattern.
The one nice thing about this pattern is that the sender does not need to change. Any class with a standard event can be a sender in this pattern.
Beyond the usability and maintainability costs, there is a runtime cost. Any time a new sender and receiver pair is attached to a WeakEventManager via AddListener, and every time an event is delivered, if a receiver has been found that has been collected (its weak reference is no longer valid), a cleanup pass is scheduled. The cleanup pass is handled via a dispatcher schedule of a delegate setup to run when the dispatcher becomes idle. This goes through and unsubscribes any senders for whom the receiver is no longer valid.
This process adds overhead that is not necessarily obvious. Every time a new object is hooked up to an event or an event is raised via the WeakEvent pattern, there is a lot that has to happen behind the scenes. The look table for events gets locked against a static lock, so events can potentially cause (short) stalls due to threading. Granted, the lock is handled intelligently in order to minimize the amount of time it’s held, but it is still an extra lock that doesn’t exist with a standard delegate. Extra processing is required to parse the entire lookup list, in order to map the appropriate sender to the appropriate receiver. Finally, once the ReceiveWeakEvent method is called, extra overhead has to happen on the receiver’s side in order to process this and map it to a standard event handler.
The weak event pattern introduced with WPF works very well, and is incredibly valuable. It’s essential for WPF’s data binding. Without this, there would be many more memory leaks occurring in data centric business applications. It can also be very useful outside of the View and ViewModel – Any time you have a long lived event listener, and the lifetime of the object listening to an event does not directly correspond to the lifetime of the object raising the event, the WeakEvent pattern can add a level of protection that is invaluable. Just realize that the gains come at a cost.
Thanks for this very informative article. One question I have though is regarding this statement:
“The cleanup pass is handled via a dispatcher schedule of a delegate setup to run when the dispatcher becomes idle. This goes through and unsubscribes any senders for whom the receiver is no longer valid.”
By this do you mean that it is not necessary to call RemoveListener before disposing the receiver? I thought we have to call RemoveListener to release any event handler on week event manager part on the sender (StopListening method).
It’s always a good idea to call RemoveListener. The cleanup pass will occur, and basically remove the listener automatically, if the sender has been finalized, and will prevent the sender from keeping the listener rooted, even if you forget.
However, calling RemoveListener keeps the cleanup pass shorter (which is important), and also “stops” the listener from receiving events, which is also important if you’re disposing the listener (or you will, if designed correctly, start getting ObjectDisposedExceptions).