Weak events in C #

Original author: Daniel Grunwald
  • Transfer

From translator


Recently, in the project where I work, we ran into a memory leak problem. After reading a lot of articles - from stories about memory management in .NET to practical recommendations on the correct release of resources, I came across an article that tells how to use events correctly. I want to introduce her translation.
This is a topic from the sandbox, with which I got here on Habr.


Introduction


When using regular events in C #, subscribing to an event creates a link from the object containing the event to the subscribing object.



If the source object lives longer than the subscriber object, memory leaks are possible: in the absence of other references to the subscriber, the source object will still refer to it. Therefore, the memory occupied by the subscribing object cannot be freed by the garbage collector.

There are many approaches to solve this problem. This article will discuss some of them, and discuss the advantages and disadvantages. I divided all approaches into two parts: in one, we will assume that the source of the event is an existing class with an ordinary event; in another, we will change the original object itself to look at the work of various methods.

What are the events?


Many developers think that events are a list of delegates. This is not true. As you know, delegates can be multicast - contain links to several functions at once:

EventHandler eh = Method1;
eh += Method2;

What then are events? They are similar to properties: inside they contain a delegate field, access to which is directly denied. The delegate’s public field (or public property) may cause the list of event handlers to be cleared by another object, or that the event will be called from the outside - while we want to call it only from the source object.

Properties are a pair of get / set methods. Events are a couple of add / remove methods.

public event EventHandler MyEvent {
   add { ... }
   remove { ... }
}

Only methods to add and remove handlers should be public. In this case, the remaining classes will not be able to get a list of handlers, they will not be able to clear it, or raise an event.

Sometimes the short syntax for declaring events in C # is misleading:

public event EventHandler MyEvent;

In fact, such a compilation entry expands to:

private EventHandler _MyEvent; // закрытое поле обработчика
public event EventHandler MyEvent {
  add { lock(this) { _MyEvent += value; } }
  remove { lock(this) { _MyEvent -= value; } }
}

In C #, events are implemented by default using synchronization, using the objects in which they are declared. You can verify this with a disassembler - the add and remove methods are marked with the [MethodImpl (MethodImplOptions.Synchronized)] attribute, which is equivalent to synchronization using the current instance of the object.

Subscribing and unsubscribing from an event are thread safe operations. However, the thread-safe event call is left to the discretion of the developers, and very often they do it incorrectly:

if (MyEvent != null)
   MyEvent(this, EventArgs.Empty);
   // может быть вызвано исключение NullReferenceException в том случае,
   // если обработчик был удален из списка уже после проверки из другого потока

Another common option is to pre-save the delegate in a local variable.

EventHandler eh = MyEvent;
if (eh != null) eh(this, EventArgs.Empty);

Is this code thread safe? It depends. According to the memory model described in the C # language specification, this example is not thread safe: the JIT compiler, by optimizing the code, can delete local variables. However, the .NET runtime (starting with version 2.0) has a stronger memory model, and in it this code is thread safe.

The correct solution, according to the ECMA specification, is to assign a local variable in the lock (this) block or use a volatile field to save a reference to the delegate.

EventHandler eh;
lock (this) { eh = MyEvent; }
if (eh != null) eh(this, EventArgs.Empty);

Part 1: Weak events on the subscriber side


In this part, we will imply that we have a normal event (with reference to handlers), and any unsubscribing from it should be done on the side of subscribers.

Solution 0: Just Unsubscribe


void RegisterEvent()
{
    eventSource.Event += OnEvent;
}
void DeregisterEvent()
{
    eventSource.Event -= OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
    ...
}

Simple and effective, which you should use whenever possible. However, it is not always possible to provide a call to the DeregisterEvent method after the object is no longer used. You can try using the Dispose method, although it is commonly used for unmanaged resources. The finalizer in this case will not work: the garbage collector will not call it, because the original object refers to our subscriber!

Benefits
Easy to use if using an object involves calling Dispose.

Disadvantages
Explicit memory management is a complicated thing. The Dispose method can also be forgotten to call.

Solution 1: Unsubscribe from an event after it is called


void RegisterEvent()
{
    eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
    if (!InUse) {
        eventSource.Event -= OnEvent;
        return;
    }
    ...
}

Now we don’t have to care if someone tells us that the subscribing object is no longer in use. We ourselves verify this after the event is called. However, if we cannot use solution 0, then, as a rule, it is impossible to determine from the object itself whether it is used. And given the fact that you are reading this article, you probably came across one of these cases.

It is worth noting that this solution already loses to solution 0: if the event is not triggered, then we will receive a memory leak occupied by the subscriber. Imagine a lot of objects subscribing to the static SettingsChanged event. Then all these objects will not be cleaned up by the garbage collector until the event fires - and this may never happen.

Benefits
None.

disadvantages
Memory leak if the event is not triggered. It is also difficult to determine if an object is in use.

Solution 2: Wrap with weak reference


This solution is almost identical to the previous one, except that we put the code of the event handler in a wrapper class, which then redirects the call to the subscribing object, accessible via a weak link . Using a weak link, you can easily check if a subscriber object still exists.



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;
    }
}

Benefits
Allows the garbage collector to free up subscriber memory.

Disadvantages
Memory leak occupied by the wrapper if the event never fails. Writing wrapper classes for each event is a bunch of repeating code.

Solution 3: Unsubscribing from an event in the finalizer


In the previous example, we stored a link to EventWrapper and had a public Deregister method. We can add a finalizer (destructor) to the subscriber and use it to unsubscribe from the event.
~ListenerObject() {
    ew.Deregister();
}

This method will save us from a memory leak, but you have to pay for it: the garbage collector spends more time to delete objects with finalizers. When a subscriber ceases to be referenced (with the exception of weak links), it will survive the first garbage collection and will be transferred to a higher generation. Then the garbage collector will call the finalizer, and only after that the object can be deleted at the next garbage collection (already in the new generation).

It should also be noted that finalizers are called in a separate thread. This may cause an error if the event subscription / unsubscription is implemented in a non-thread safe manner. Remember that the default implementation of events in C # is not thread safe!

Benefits
Allows the garbage collector to free up subscriber memory. There is no memory leak occupied by the wrapper.

Disadvantages
The presence of a finalizer increases the time that an unused object will remain in memory before it is deleted. A thread safe implementation of the event is required. Lots of duplicate code.

Solution 4: Reuse the wrapper


The code below contains a wrapper class that can be reused. Using lambda expressions, we pass a different code: to subscribe to an event, unsubscribe from it, and to transfer 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 instance of eventWrapper has only one public method - Deregister. We need to be careful when writing lambda expressions: since they are compiled into delegates, they can also contain references to objects. That is why the subscriber returns as me. If we wrote (me, sender, args) => this.OnEvent (sender, args), then the lambda expression would attach to the this variable, thereby causing the creation of a closure. And since WeakEventHandler contains a link to the delegate that raises the event, this would lead to a “strong” (regular) link from the wrapper to the subscriber. Fortunately, we have the opportunity to check whether the delegate captured any variables: for such lambda expressions, the compiler will create instance methods; otherwise the methods will be static. WeakEventHandler verifies this using the Delegate.Method flag.

This approach allows you to reuse the wrapper, but still requires its own wrapper class for each type of delegate. Since you can actively use System.EventHandler and System.EventHandler, if you have many different types of delegates, you will want to automate all this. You can use code generation or System.Reflection.Emit space types for this.

Benefits
Allows the garbage collector to free up subscriber memory. Not very large size of the additional code.

Disadvantages
The memory leak occupied by the wrapper in case the event never fails.

Solution 5: WeakEventManager


WPF has built-in support for subscriber-side weak events through the WeakEventManager class. It works similarly to previous solutions using wrappers, except that a single instance of WeakEventManager serves as a wrapper between multiple event sources and multiple subscribers. Due to the fact that there is only one instance of the object, WeakEventManager avoids memory leaks even if the event is not triggered: subscribing to another event may cause the list of old subscriptions to be cleared. These cleanups are performed by the WPF manager on threads in which the WPF message loop is running.

WeakEventManager also has an additional limitation: it requires the correct setting of the sender parameter. If you use it for the button.Click event, then only events with sender == button will be passed to subscribers. Some event implementations may attach handlers to other events:
public event EventHandler Event {
    add { anotherObject.Event += value; }
    remove { anotherObject.Event -= value; }
}

Such events cannot be used in the WeakEventManager.

One WeakEventManager per event, one instance per thread. The recommended template for determining such events with code blanks can be found in the article "WeakEvent Templates" on MSDN.

Fortunately, we can simplify this with generics:
public sealed class ButtonClickEventManager
    : WeakEventManagerBase
{
    protected override void StartListening(Button source)
    {
        source.Click += DeliverEvent;
    }
    protected override void StopListening(Button source)
    {
        source.Click -= DeliverEvent;
    }
}

Note that DeliverEvent accepts (object, EventArgs) as arguments, while the Click event provides arguments (object, RoutedEventArgs). In C # there is no support for converting between types of delegates, however there is support for contraception when creating delegates from a group of methods .

Benefits
Allows the garbage collector to free up subscriber memory. The memory occupied by the wrapper can also be freed.

Disadvantages
The method is not well suited for applications where there is no graphical interface, since the implementation is bound to WPF.

Part 2: Weak events on the source side


In this part, we will look at ways to implement weak events by modifying the original object containing the event. All the solutions proposed below have an advantage over the implementation of weak events on the side of subscribers: we can easily make a thread-safe subscription / unsubscription.

Solution 0: Interface


WeakEventManager is worth mentioning in this part. As a wrapper, it joins ordinary events (the subscriber side), but can also provide weak events to clients (the source object side).

There is an IWeakEventListener interface. For subscribers implementing this interface, the source object will be referenced using a weak link, and the implemented ReceiveWeakEvent method will be called.

Benefits
Simple and effective.

Disadvantages
When an object is subscribed to many events, in the implementation of ReceiveWeakEvent you will have to write a bunch of checks on the types of events and the source.

Solution 1: Weak delegate reference


This is another approach used in WPF: CommandManager. InvalidateRequery looks like a regular event, but it is not. It contains a weak reference to the delegate, so subscribing to a static event does not lead to a memory leak.



This is a simple solution, but event subscribers can easily forget about it or misunderstand:
CommandManager.InvalidateRequery += OnInvalidateRequery;
// или
CommandManager.InvalidateRequery += new EventHandler(OnInvalidateRequery);

The problem is that the CommandManager contains only a weak delegate link, and the subscriber does not contain any delegate links at all. Therefore, at the next garbage collection, the delegate will be deleted and OnInvalidateRequery will no longer work, even if the subscribing object is still in use. The delegate must be responsible for the fact that the delegate will live in memory.



class Listener {
    EventHandler strongReferenceToDelegate;
    public void RegisterForEvent()
    {
        strongReferenceToDelegate = new EventHandler(OnInvalidateRequery);
        CommandManager.InvalidateRequery += strongReferenceToDelegate;
    }
    void OnInvalidateRequery(...) {...}
}

Benefits
Free up delegate memory.

Disadvantages
If you forget to affix a “strong” link to the delegate, the event will fire until the first garbage collection. This can make it difficult to find errors.

Solution 2: Object + Forwarder


While WeakEventManager was adapted for solution 0, the WeakEventHandler wrapper is adapted in this solution: pair registration .


eventSource.AddHandler(this, (me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));

Benefits
Simple and effective.

Disadvantages
Unusual way of recording events; a redirect lambda expression requires type conversion.

Solution 3: SmartWeakEvent


The SmartWeakEvent, presented below, provides an event that looks like a normal .NET event but stores a weak subscriber link. T.O. there is no need to keep a “strong” link to the delegate.

void RegisterEvent()
{
    eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
    ...
}

Define the event:

SmartWeakEvent _event = new SmartWeakEvent();
public event EventHandler Event {
    add { _event.Add(value); }
    remove { _event.Remove(value); }
}
public void RaiseEvent()
{
    _event.Raise(this, EventArgs.Empty);
}

How it works? Using the properties of Delegate.Target and Delegate.Method, each delegate is divided into a target object (stored using a weak reference) and MethodInfo. When an event fires, the method is called using reflection.



The vulnerability of this method is that someone could attach an anonymous method as an event handler.

int localVariable = 42;
eventSource.Event += delegate { Console.WriteLine(localVariable); };

In this case, the target is a closure that can be immediately removed by the collector, because there are no links to it. However, SmartWeakEvent can detect such cases and throw an exception, so you should not have any problems debugging, because the event handler is untied earlier than you think.

if (d.Method.DeclaringType.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length != 0)
    throw new ArgumentException(...);

Benefits
It really looks like a weak event; almost no redundant code.

Disadvantages The
implementation of methods through reflection is rather slow. It does not work with partial permission, because it implements private methods.

Solution 4: FastSmartWeakEvent


The functionality and use are similar to the solution with SmartWeakEvent, but the performance is much higher.

Here are the test results for an event with two delegates (one refers to an instance method, the other refers to a static):

Normal (strong) event ... 16 948 785 calls per second
Smart weak event ... 91 960 calls per second
Fast smart weak event ... 4 901 840 calls per second

How does it work? We no longer use reflection to execute the method. Instead, we compile the method (similar to the method in the previous solution) during the execution of the program using System.Reflection.Emit.DynamicMethod.

Benefits
Looks like a real weak event; almost no redundant code.

disadvantages
It does not work with partial permission, because it implements private methods.

suggestions


  • Use the WeakEventManager for everything that works in the GUI thread in WPF applications (for example, for user controls that subscribe to model object events)
  • Use FastSmartWeakEvent if you want to provide a weak event
  • Use WeakEventHandler if you want to subscribe to the event.

Also popular now: