Weak Events in C#
INTRODUCTION
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 amemory 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":
Hide Copy Code
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.
Hide Copy Code
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:
Hide Copy Code
public event EventHandler MyEvent;
This expands to:
Hide Copy Code
private EventHandler _MyEvent; // the underlying field
// this isn't actually named "_MyEvent" but also "MyEvent",
// but then you couldn't see the difference 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
this
! You can verify this with a disassembler - the add
andremove
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:
Hide Copy Code
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.
Hide Copy Code
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 isnot 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.
Hide Copy Code
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 EVENTS
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.
Solution 0: Just Deregister
Hide Copy Code
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
.Solution 1: Deregister When the Event is Called
Hide Copy Code
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.Solution 2: Wrapper with Weak Reference
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.
Hide Shrink Copy Code
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 Finalizer
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.
Hide Copy Code
~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 arenot 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 Wrapper
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.
Hide Copy Code
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 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 exception if you use it incorrectly.
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 static
method for lambda expressions that don't. WeakEventHandler
checks this using Delegate.Method.IsStatic
, and will throw an
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.
Solution 5: WeakEventManager
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, theWeakEventManager
can avoid the leak when the event is never called: registering another event on aWeakEventManager
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 withsender==button
will be delivered. Some event implementations may simply attach the handlers to another event:
Hide Copy Code
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:
Hide Copy Code
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# supportscontravariance 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.
PART 2: SOURCE-SIDE WEAK EVENTS
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.
Solution 0: Interface
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.Advantages
Simple and effective.
Disadvantages
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.Solution 1: WeakReference to Delegate
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:
Hide Copy Code
CommandManager.InvalidateRequery += OnInvalidateRequery;
//or
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.
Hide Copy Code
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.
Solution 2: object + Forwarder
While solution 0 was adapted from the
WeakEventManager
, this solution is adapted from theWeakEventHandler
wrapper: register an object,ForwarderDelegate
pair.
Hide Copy Code
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.
Solution 3: SmartWeakEvent
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.
Hide Copy Code
void RegisterEvent()
{
eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
...
}
Event definition:
Hide Copy Code
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.
Hide Copy Code
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.
Hide Copy Code
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.Solution 4: FastSmartWeakEvent
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):
Hide Copy Code
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.SUGGESTIONS
- 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
.
0 comments:
Post a Comment