|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Table of contents
IntroductionWhen using normal C# events, registering an event handler creates a strong reference from the event source to the listening object.
If the source object has a longer lifetime than the listener, and the listener doesn't need the events anymore when there are no other references to it, using normal .NET events causes a memory leak: the source object holds listener objects in memory that should be garbage collected. There are lots of different approaches to this problem. This article will explain some of them and discuss their advantages and disadvantages. I have sorted the approaches in two categories: first, we will assume that the event source is an existing class with a normal C# event; after that, we will allow modifying the event source to allow different approaches. What exactly are events?Many programmers think events are a list of delegates - that's simply wrong. Delegates themselves have the ability to be "multi-cast": EventHandler eh = Method1;
eh += Method2;
So, what then are events? Basically, they are like properties: they encapsulate a delegate field and restrict access to it. A public delegate field (or a public delegate property) could mean that other objects could clear the list of event handlers, or raise the event - but we want only the object defining the event to be able to do that. Properties essentially are a pair of public event EventHandler MyEvent {
add { ... }
remove { ... }
}
Only adding and removing handlers is public. Other classes cannot request the list of handlers, cannot clear the list, or cannot call the event. Now, what leads to confusion sometimes is that C# has a short-hand syntax: public event EventHandler MyEvent;
This expands to: private EventHandler _MyEvent; // the underlying field
// this isn't actually named "_MyEvent" but also "MyEvent",
// but then you couldn't see the different between the field
// and the event.
public event EventHandler MyEvent {
add { lock(this) { _MyEvent += value; } }
remove { lock(this) { _MyEvent -= value; } }
}
Yes, the default C# events are locking on Registering and deregistering events is thread-safe. However, raising the event in a thread-safe manner is left to the programmer writing the code that raises the event, and often gets done incorrectly: the raising code that's probably used the most is not thread-safe: if (MyEvent != null)
MyEvent(this, EventArgs.Empty);
// can crash with a NullReferenceException
// when the last event handler is removed concurrently.
The second most commonly seen strategy is first reading the event delegate into a local variable. EventHandler eh = MyEvent;
if (eh != null) eh(this, EventArgs.Empty);
Is this thread-safe? Answer: it depends. According to the memory model in the C# specification, this is not thread-safe. The JIT compiler is allowed to eliminate the local variable, see Understand the Impact of Low-Lock Techniques in Multithreaded Apps [^]. However, the Microsoft .NET runtime has a stronger memory model (starting with version 2.0), and there, that code is thread-safe. It happens to be also thread-safe in Microsoft .NET 1.0 and 1.1, but that's an undocumented implementation detail. A correct solution, according to the ECMA specification, would have to move the assignment to the local variable into a EventHandler eh;
lock (this) { eh = MyEvent; }
if (eh != null) eh(this, EventArgs.Empty);
This means we'll have to distinguish between events that are thread-safe and events that are not thread-safe. Part 1: Listener-side weak eventsIn this part, we'll assume the event is a normal C# event (strong references to event handlers), and any cleanup will have to be done on the listening side. Solution 0: Just deregistervoid RegisterEvent()
{
eventSource.Event += OnEvent;
}
void DeregisterEvent()
{
eventSource.Event -= OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
...
}
Simple and effective, this is what you should use when possible. But, often, it's not trivially possible to ensure the Advantages:Simple if the object already has a notion of being disposed. Disadvantages:Explicit memory management is hard, code can forget to call Solution 1: Deregister when the event is calledvoid RegisterEvent()
{
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
if (!InUse) {
eventSource.Event -= OnEvent;
return;
}
...
}
Now, we don't require that someone tells us when the listener it is no longer in use: it just checks this itself when the event is called. However, if we cannot use solution 0, then usually, it's also not possible to determine " But, this "solution" already has an important disadvantage over solution 0: if the event is never fired, then we'll leak listener objects. Imagine that lots of objects register to a static " Advantages:- Disadvantages:Leaks when the event never fires; usually, " Solution 2: Wrapper with weak referenceThis solution is nearly identical to the previous, except that we move the event handling code into a wrapper class that forwards the calls to a listener instance which is referenced with a weak reference. This weak reference allows for easy detection if the listener is still alive.
EventWrapper ew;
void RegisterEvent()
{
ew = new EventWrapper(eventSource, this);
}
void OnEvent(object sender, EventArgs e)
{
...
}
sealed class EventWrapper
{
SourceObject eventSource;
WeakReference wr;
public EventWrapper(SourceObject eventSource,
ListenerObject obj) {
this.eventSource = eventSource;
this.wr = new WeakReference(obj);
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
ListenerObject obj = (ListenerObject)wr.Target;
if (obj != null)
obj.OnEvent(sender, e);
else
Deregister();
}
public void Deregister()
{
eventSource.Event -= OnEvent;
}
}
Advantages:Allows garbage collection of the listener object. Disadvantages:Leaks the wrapper instance when the event never fires; writing a wrapper class for each event handler is a lot of repetitive code. Solution 3: Deregister in finalizerNote that we stored a reference to the ~ListenerObject() {
ew.Deregister();
}
That should take care of our memory leak, but it comes at a cost: finalizable objects are expensive for the garbage collector. When there are no references to the listener object (except for the weak reference), it'll survive the first garbage collection (and move to a higher generation), have the finalizer run, and then can only be collected after the next garbage collection (of the new generation). Also, finalizers run on the finalizer thread; this may cause problems if registering/deregistering events on an event source is not thread-safe. Remember, the default events generated by the C# compiler are not thread-safe! Advantages:Allows garbage collection of the listener object; does not leak wrapper instances. Disadvantages:Finalizer delays GC of listener; requires thread-safe event source; lots of repetitive code. Solution 4: Reusable wrapperThe code download contains a reusable version of the wrapper class. It works by taking the lambda expressions for the code parts that need to be adapted to a specific use: Register event handler, deregister event handler, forward the event to a private method. eventWrapper = WeakEventHandler.Register(
eventSource,
(s, eh) => s.Event += eh, // registering code
(s, eh) => s.Event -= eh, // deregistering code
this, // event listener
(me, sender, args) => me.OnEvent(sender, args) // forwarding code
);
The returned This approach is fairly reusable, but it still requires a wrapper class for each delegate type. While you can get pretty far with Advantages:Allows garbage collection of listener object; code overhead not too bad. Disadvantages:Leaks wrapper instance when event never fires. Solution 5: WeakEventManagerWPF has built-in support for listener-side weak events, using the Also, the public event EventHandler Event {
add { anotherObject.Event += value; }
remove { anotherObject.Event -= value; }
}
Such events cannot be used with There is one Fortunately, we can simplify this with Generics: public sealed class ButtonClickEventManager
: WeakEventManagerBase<ButtonClickEventManager, Button>
{
protected override void StartListening(Button source)
{
source.Click += DeliverEvent;
}
protected override void StopListening(Button source)
{
source.Click -= DeliverEvent;
}
}
Note that Advantages:Allows garbage collection of listener object; does not leak wrapper instances. Disadvantages:Tied to a WPF dispatcher, cannot be easily used on non-UI-threads. Part 2: Source-side weak eventsHere, we'll take a look at ways to implement weak events by modifying the event source. All these have a common advantage over the listener-side weak events: we can easily make registering/deregistering handlers thread-safe. Solution 0: InterfaceThe In the
Advantages:Simple and effective. Disadvantages:When a listener handles multiple events, you end up with lots of conditions in the Solution 1: WeakReference to delegateThis is another approach to weak events used in WPF:
This is a simple solution, but it's easy for event consumers to forget about it and get it wrong: CommandManager.InvalidateRequery += OnInvalidateRequery;
//or
CommandManager.InvalidateRequery += new EventHandler(OnInvalidateRequery);
The problem here is that the
class Listener {
EventHandler strongReferenceToDelegate;
public void RegisterForEvent()
{
strongReferenceToDelegate = new EventHandler(OnInvalidateRequery);
CommandManager.InvalidateRequery += strongReferenceToDelegate;
}
void OnInvalidateRequery(...) {...}
}
Advantages:Doesn't leak delegate instances. Disadvantages:Easy to get wrong: forgetting the strong reference to the delegate causes events to fire only until the next garbage collection. This can result in hard-to-find bugs. Solution 2: object + ForwarderWhile solution 0 was adapted from the
eventSource.AddHandler(this,
(me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));
Advantages:Simple and effective. Advantages:Unusual signature for registering events; forwarding lambda expressions require cast. Solution 3: SmartWeakEventThe void RegisterEvent()
{
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
...
}
Event definition: SmartWeakEvent<EventHandler> _event
= new SmartWeakEvent<EventHandler>();
public event EventHandler Event {
add { _event.Add(value); }
remove { _event.Remove(value); }
}
public void RaiseEvent()
{
_event.Raise(this, EventArgs.Empty);
}
How does it work? Using the
A possible problem here is that someone might try to attach an anonymous method as an event handler that captures a variable. int localVariable = 42;
eventSource.Event += delegate { Console.WriteLine(localVariable); };
In this case, the delegate's target object is the closure, which can be immediately collected because there are no other references to it. However, the if (d.Method.DeclaringType.GetCustomAttributes(
typeof(CompilerGeneratedAttribute), false).Length != 0)
throw new ArgumentException(...);
Advantages:Looks like a real weak event; nearly no code overhead. Disadvantages:Invocation using Reflection is slow. Solution 4: FastSmartWeakEventThe functionality and usage is identical to the Here are the benchmark results of an event with two registered delegates (one instance method and one static method): Normal (strong) event... 16948785 calls per second
Smart weak event... 91960 calls per second
Fast smart weak event... 4901840 calls per second
How does it work? We're not using Reflection anymore to call the method. Instead, we're compiling a forwarder method (similar to the "forwarding code" in the previous solutions) at runtime using Advantages:Looks like a real weak event; nearly no code overhead. Disadvantages:- Suggestions
| ||||||||||||||||||||