Table of contents
When 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.
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 get
/set
-methods. Events are just a pair of add
/remove
-methods.
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;
public event EventHandler MyEvent {
add { lock(this) { _MyEvent += value; } }
remove { lock(this) { _MyEvent -= value; } }
}
Yes, the default C# events are locking on this
! You can verify this with a disassembler - the add
and remove
methods are decorated with [MethodImpl(MethodImplOptions.Synchronized)]
, which is equivalent to locking on this
.
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);
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 lock(this)
block or use a volatile
field to store the delegate.
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.
In 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.
void 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 DeregisterEvent
method is called whenever the object is no longer in use. You might try the Dispose pattern, though that's usually meant for unmanaged resources. A finalizer will not work: the garbage collector won't call it because the event source still holds a reference to our object!
Advantages
Simple if the object already has a notion of being disposed.
Disadvantages
Explicit memory management is hard, code can forget to call Dispose
.
void 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 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 "InUse
" from within the listener object. And given that you are reading this article, you've probably come across one of those cases.
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
"SettingsChanged
" event - all these objects cannot be garbage collected until a setting is changed - which might never happen in the program's lifetime.
Advantages
None.
Disadvantages
Leaks when the event never fires; usually, "InUse
" cannot be easily determined.
This 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.
Note that we stored a reference to the EventWrapper
and had a public Deregister
method. We can add a finalizer to the listener and use that to deregister from the event.
~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.
The 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,
(s, eh) => s.Event -= eh,
this,
(me, sender, args) => me.OnEvent(sender, args)
);
The returned eventWrapper
exposes a single public
method: Deregister
. Now, we need to be careful with lambda expressions, since they are compiled to delegates that may contain further object references. That's why the event listener is passed back as "me
". Had we written (me, sender, args) => this.OnEvent(sender, args)
, the lambda expression would have captured the "this
" variable, causing a closure object to be generated. Since the WeakEventHandler
stores a reference to the forwarding delegate, this would have caused a strong reference from the wrapper to the listener. Fortunately, it's possible to check whether a delegate captures any variables: the compiler will generate an instance method for lambda expressions that capture variables, and a static
method for lambda expressions that don't. WeakEventHandler
checks this using Delegate.Method.IsStatic
, and will throw an exception if you use it incorrectly.
This approach is fairly reusable, but it still requires a wrapper class for each delegate type. While you can get pretty far with System.EventHandler
and System.EventHandler<T>
, you might want to automate this when there are lots of different delegate types. This could be done at compile-time using code generation, or at runtime using System.Reflection.Emit
.
Advantages
Allows garbage collection of listener object; code overhead not too bad.
Disadvantages
Leaks wrapper instance when event never fires.
WPF has built-in support for listener-side weak events, using the WeakEventManager
class. It works similar to the previous wrapper solutions, except that a single WeakEventManager
instance serves as a wrapper between multiple sender and multiple listeners. Due to this single instance, the WeakEventManager
can avoid the leak when the event is never called: registering another event on a WeakEventManager
can trigger a clean-up of old events. These clean-ups are scheduled using the WPF dispatcher, they will occur only on threads running a WPF message loop.
Also, the WeakEventManager
has a restriction that our previous solutions didn't have: it requires the sender parameter to be set correctly. If you use it to attach to button.Click
, only events with sender==button
will be delivered. Some event implementations may simply attach the handlers to another event:
public event EventHandler Event {
add { anotherObject.Event += value; }
remove { anotherObject.Event -= value; }
}
Such events cannot be used with WeakEventManager
.
There is one WeakEventManager
class per event, each with an instance per thread. The recommended pattern for defining these events is a lot of boilerplate code: see "WeakEvent Patterns" on MSDN [^].
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 DeliverEvent
takes (object, EventArgs)
, whereas the Click
event provides (object, RoutedEventArgs)
. While there is no conversion between delegate types, C# supports contravariance when creating delegates from method groups [^].
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.
Here, 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.
The WeakEventManager
also deserves to be mentioned in this section: as a wrapper, it attaches ("listening-side") to normal C# events, but it also provides ("source-side") a weak event to clients.
In the WeakEventManager
, this is the IWeakEventListener
interface. The listening object implements an interface, and the source simply has a weak reference to the listener and calls the interface method.
Simple and effective.
When a listener handles multiple events, you end up with lots of conditions in the HandleWeakEvent
method to filter on event type and on event source.
This is another approach to weak events used in WPF: CommandManager.InvalidateRequery
looks like a normal .NET event, but it isn't. It holds only a weak reference to the delegate, so registering to that static
event does not cause memory leaks.
This is a simple solution, but it's easy for event consumers to forget about it and get it wrong:
CommandManager.InvalidateRequery += OnInvalidateRequery;
CommandManager.InvalidateRequery += new EventHandler(OnInvalidateRequery);
The problem here is that the CommandManager
only holds a weak reference to the delegate, and the listener doesn't hold any reference to it. So, on the next GC run, the delegate will be garbage collected, and OnInvalidateRequery
doesn't get called anymore even if the listener object is still in use. To ensure the delegate survives long enough, the listener is responsible for keeping a reference to it.
class Listener {
EventHandler strongReferenceToDelegate;
public void RegisterForEvent()
{
strongReferenceToDelegate = new EventHandler(OnInvalidateRequery);
CommandManager.InvalidateRequery += strongReferenceToDelegate;
}
void OnInvalidateRequery(...) {...}
}
WeakReferenceToDelegate
in the source-code download shows an example event implementation that is thread-safe and cleans the handler list when another handler is added.
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.
While solution 0 was adapted from the WeakEventManager
, this solution is adapted from the WeakEventHandler
wrapper: register an object,ForwarderDelegate
pair.
eventSource.AddHandler(this,
(me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));
Advantages
Simple and effective.
Disadvantages
Unusual signature for registering events; forwarding lambda expressions require cast.
The SmartWeakEvent
in the source code download provides an event that looks like a normal .NET event, but keeps weak references to the event listener. It does not suffer from the "must keep reference to delegate"-problem.
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 Delegate.Target
and Delegate.Method
properties, each delegate is split up into a target (stored as a weak reference) and the MethodInfo
. When the event is raised, the method is invoked using Reflection.
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 SmartWeakEvent
can detect this case and will throw an exception, so you won't have any difficulty to debug problems because the event handler is deregistered before you think it should be.
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; does not work in partial trust because it uses reflection on private
methods.
The functionality and usage is identical to the SmartWeakEvent
, but the performance is dramatically improved.
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 System.Reflection.Emit.DynamicMethod
.
Advantages
Looks like a real weak event; nearly no code overhead.
Disadvantages
Does not work in partial trust because it uses reflection on private
methods.
- For anything running on the UI thread in WPF applications (e.g., custom controls that attach events on the model objects), use the
WeakEventManager
. - If you want to provide a weak event, use
FastSmartWeakEvent
. - If you want to consume an event, use
WeakEventHandler
.
- 24 Apr 2009: code updated (bug fixes)
- Incorrect 'Derived from EventArgs' check reported by olivianer and Fintan
- Type safety issue with
FastSmartWeakEvent
- 5 Oct 2008: Article published