Introduction
Some time ago, I needed to register quite a lot of event handlers. At that point, I got aware of the memory leaking if you do not unregister unneeded event handlers. The reason is that such a handler's instance keeps being referenced by the corresponding event dispatcher, preventing the garbage collector from garbage collecting that corresponding instance.
Another related problem was that unneeded handlers kept being executed since their instances could not be garbage collected, resulting in loss of performance. So unregistering events is an important thing to do if you have the opportunity to do that.
But of course, it would be comfortable not to worry about unregistering events at all.
I discovered this to be a fairly common issue and I found many solutions and some nice articles on the web referring to the WeakReference
and WeakEventManager
classes. These solutions partly worked for me as they dealt with the memory leakage. But for each, I encountered performance or usability issues, and I decided to write a simple weak event dispatcher with focus on usability and performance.
Background
A weak reference is a reference which does not count for the garbage collector when it needs to decide about garbage collecting the corresponding instance. This implies that a weak reference can be null at a sudden moment because it was garbage collected as there were no other strong references anymore to that particular instance. This also means that you need to take care when using weak references.
Using the code
This WeakEventDispatcher
acts like a "normal" event dispatcher on the outside, i.e., you can add and remove handlers easily. On the inside, handlers are bucketed together with weak references to their instances. The invocation is done by compiled lambda expressions, i.e., delegates. These delegates are cached and reused where possible. The purging (getting rid of handlers belonging to garbage collected instances) is done on a regular basis which can be configured by a threshold setting. As a result, there is no memory leakage anymore, and the event invocation performance increases significantly if you have to deal with a lot of event handling.
A unit test class is included which shows how to deal with the WeakEventDispatcher
. Further, a test method is included for performance measurement. It shows the performance differences between applying standard .NETevent handling and applying the weak event dispatcher.
The performance test tells that you gain more performance benefit as you increase the number of event handling iterations. If you largely increase the number of iterations, the standard .NET event handling eventually entangles by the number of handler invocations since "lost" instances are not garbage collected and as a result the corresponding handlers keep being invoked. The WeakEventDispatcher
prevents that from happening.
The following example shows how to apply the WeakEventDispatcher
:
public class Entity {
private readonly WeakEventDispatcher<EventArgs> _changeNotificationDispatcher;
public event EventHandler<EventArgs> DataChanged {
add { _changeNotificationDispatcher += value; }
remove { _changeNotificationDispatcher -= value; }
}
protected virtual void OnDataChanged(EventArgs e) {
if(_changeNotificationDispatcher!= null)
_changeNotificationDispatcher.Invoke(this, e);
}
}
Inside the event dispatcher, some housekeeping needs to be done once in a while, i.e., the handlers which belonged to garbage collected instances need to be removed. The housekeeping is performed by Purge()
. The purging can be performed manually, but is done regularly as well every configurable number of times that a public dispatcher method is called. The interval is configured by the PurgeThreshold
, and the corresponding value can be passed with the dispatcher's constructor. You could instantiate the dispatcher at declaration and pass a purge threshold setting, like:
private readonly WeakEventDispatcher<EventArgs>
_changeNotificationDispatcher = new WeakEventDispatcher<EventArgs>(10);
Now you set the threshold to 10, meaning that a purge is performed every ten Invoke(...)
or +=
calls. So with this value, you define how frequently Purge()
is performed. If you pass 0, then Purge()
is performed each time you touch any public dispatcher method. By default, the purge threshold is set to 5.