简体   繁体   中英

Using WeakReference to resolve issue with .NET unregistered event handlers causing memory leaks

The problem: Registered event handlers create a reference from the event to the event handler's instance. If that instance fails to unregister the event handler (via Dispose, presumably), then the instance memory will not be freed by the garbage collector.

Example:

    class Foo
    {
        public event Action AnEvent;
        public void DoEvent()
        {
            if (AnEvent != null)
                AnEvent();
        }
    }        
    class Bar
    {
        public Bar(Foo l)
        {
            l.AnEvent += l_AnEvent;
        }

        void l_AnEvent()
        {

        }            
    }

If I instantiate a Foo, and pass this to a new Bar constructor, then let go of the Bar object, it will not be freed by the garbage collector because of the AnEvent registration.

I consider this a memory leak, and seems just like my old C++ days. I can, of course, make Bar IDisposable, unregister the event in the Dispose() method, and make sure to call Dispose() on instances of it, but why should I have to do this?

I first question why events are implemented with strong references? Why not use weak references? An event is used to abstractly notify an object of changes in another object. It seems to me that if the event handler's instance is no longer in use (ie, there are no non-event references to the object), then any events that it is registered with should automatically be unregistered. What am I missing?

I have looked at WeakEventManager. Wow, what a pain. Not only is it very difficult to use, but its documentation is inadequate (see http://msdn.microsoft.com/en-us/library/system.windows.weakeventmanager.aspx -- noticing the "Notes to Inheritors" section that has 6 vaguely described bullets).

I have seen other discussions in various places, but nothing I felt I could use. I propose a simpler solution based on WeakReference, as described here. My question is: Does this not meet the requirements with significantly less complexity?

To use the solution, the above code is modified as follows:

    class Foo
    {
        public WeakReferenceEvent AnEvent = new WeakReferenceEvent();

        internal void DoEvent()
        {
            AnEvent.Invoke();
        }
    }

    class Bar
    {
        public Bar(Foo l)
        {
            l.AnEvent += l_AnEvent;
        }

        void l_AnEvent()
        {

        }
    }

Notice two things: 1. The Foo class is modified in two ways: The event is replaced with an instance of WeakReferenceEvent, shown below; and the invocation of the event is changed. 2. The Bar class is UNCHANGED.

No need to subclass WeakEventManager, implement IWeakEventListener, etc.

OK, so on to the implementation of WeakReferenceEvent. This is shown here. Note that it uses the generic WeakReference<T> that I borrowed from here: http://damieng.com/blog/2006/08/01/implementingweakreferencet

class WeakReferenceEvent
{
    public static WeakReferenceEvent operator +(WeakReferenceEvent wre, Action handler)
    {
        wre._delegates.Add(new WeakReference<Action>(handler));
        return wre;
    }

    List<WeakReference<Action>> _delegates = new List<WeakReference<Action>>();

    internal void Invoke()
    {
        List<WeakReference<Action>> toRemove = null;
        foreach (var del in _delegates)
        {
            if (del.IsAlive)
                del.Target();
            else
            {
                if (toRemove == null)
                    toRemove = new List<WeakReference<Action>>();
                toRemove.Add(del);
            }
        }
        if (toRemove != null)
            foreach (var del in toRemove)
                _delegates.Remove(del);
    }
}

It's functionality is trivial. I override operator + to get the += syntactic sugar matching events. This creates WeakReferences to the Action delegate. This allows the garbage collector to free the event target object (Bar in this example) when nobody else is holding on to it.

In the Invoke() method, simply run through the weak references and call their Target Action. If any dead (ie, garbage collected) references are found, remove them from the list.

Of course, this only works with delegates of type Action. I tried making this generic, but ran into the missing where T : delegate in C#!

As an alternative, simply modify class WeakReferenceEvent to be a WeakReferenceEvent<T>, and replace the Action with Action<T>. Fix the compiler errors and you have a class that can be used like so:

    class Foo
    {
        public WeakReferenceEvent<int> AnEvent = new WeakReferenceEvent<int>();

        internal void DoEvent()
        {
            AnEvent.Invoke(5);
        }
    }

The full code with <T>, and the operator - (for removing events) is shown here:

class WeakReferenceEvent<T>
{
    public static WeakReferenceEvent<T> operator +(WeakReferenceEvent<T> wre, Action<T> handler)
    {
        wre.Add(handler);
        return wre;
    }
    private void Add(Action<T> handler)
    {
        foreach (var del in _delegates)
            if (del.Target == handler)
                return;
        _delegates.Add(new WeakReference<Action<T>>(handler));
    }

    public static WeakReferenceEvent<T> operator -(WeakReferenceEvent<T> wre, Action<T> handler)
    {
        wre.Remove(handler);
        return wre;
    }
    private void Remove(Action<T> handler)
    {
        foreach (var del in _delegates)
            if (del.Target == handler)
            {
                _delegates.Remove(del);
                return;
            }
    }

    List<WeakReference<Action<T>>> _delegates = new List<WeakReference<Action<T>>>();

    internal void Invoke(T arg)
    {
        List<WeakReference<Action<T>>> toRemove = null;
        foreach (var del in _delegates)
        {
            if (del.IsAlive)
                del.Target(arg);
            else
            {
                if (toRemove == null)
                    toRemove = new List<WeakReference<Action<T>>>();
                toRemove.Add(del);
            }
        }
        if (toRemove != null)
            foreach (var del in toRemove)
                _delegates.Remove(del);
    }
}

Hopefully this will help someone else when they run into the mystery event caused memory leak in a garbage collected world!

I found the answer to my question as to why this doesn't work. Yes, indeed, I am missing a minor detail: The call to += to register the event (l.AnEvent += l_AnEvent;) creates an implicit Action object. This object is normally held only by the event itself (and the stack of the calling function). Thus, when the call returns and the garbage collector runs, the implicitly created Action object is freed (only a weak reference is point to it now), and the event is unregistered.

A (painful) solution is to hold a reference to the Action object as follows:

    class Bar
    {
        public Bar(Foo l)
        {
            _holdAnEvent = l_AnEvent;
            l.AnEvent += _holdAnEvent;
        }
        Action<int> _holdAnEvent;
        ...
    }

This works, but removes the simplicity of the solution.

Surely this would have a performance impact.

Its a bit like, why reference other assemblies in my solution when I can use reflection to dynamically read the assembly and make the relevant calls in it's types?

So in short ... you use a strong reference for 2 reasons ... 1. Type safety (not really applicable here) 2. Performance

This goes back to a similar debate on generics over hashtables. Last time I saw that argument put to the table however the poster was looking a the generated msil pre-jitted, maybe that can lend you some light on the problem?

One other thought though ... What if you attached an event handler to say a com object event? Technically that object is not managed so how does that know when it needs cleaning up, surely this boils down to how the framework handles scope right?

This post comes with the "it works in my head garantee", no responsibility is taken for how this post is portrayed :)

When you have an event handler, you have two objects:

  1. The object of your class. (An instance of foo)
  2. The object that represents your event handler. (For example, an instance of EventHandler or an instance of Action.)

The reason why you think you have a memory leak is that the EventHandler (or Action) object internally holds a reference to your Foo object. This will prevent your Foo object from being collected.

Now, why can't you write a WeakEventHandler? The answer is you can, but you basically have to make sure that:

  1. Your delegate (instance of EventHandler or Action) never has a hard reference to your Foo object
  2. Your WeakEventHandler holds a strong reference to your delegate, and a weak reference to your Foo object
  3. You have provisions to eventually unregister your WeakEventHandler when the weak reference becomes invalid. This is because there's no way to know when an object is collected.

In practice, this isn't worth it. This is because you have tradeoffs:

  • Your event handler method will need to be static and take the object as an argument, this way it doesn't hold a strong reference to the object you want collected.
  • Your WeakEventHandler and Action objects are at a high risk for making it to Gen 1 or Gen 2. This will cause high load in the garbage collector.
  • WeakReferences hold a GC Handle. This might negatively impact performance.

Thus, making sure to correctly unregister your event handlers is a better solution. The syntax is simpler, memory use is better, and the application performs better.

There are two patterns I know of for making weak event subscriptions: one is to have the event subscriber hold a strong reference to a delegate that points to him, while the publisher holds a weak reference to that delegate. This has the disadvantage of requiring all event firing to be done through a weak reference; it may gives the publisher no notice of whether any events have expired.

Another approach is to give everyone who's actually interested in an object a reference to a wrapper, which in turn holds a reference to the "guts"; the event handler only has a reference to the "guts", and the guts do not hold a strong reference to the wrapper. The wrapper also holds a reference to an object whose Finalize method will unsubscribe from the event (the simplest way to do this is have a simple class whose Finalize method calls an Action<Boolean> with a value of False, and whose Dispose method calls that delegate with a value of True and suppresses finalization).

This approach has the disadvantage of requiring all non-event operations on the main class to be done through a wrapper (an extra strong reference), but avoids having to use any WeakReferences for anything other than event subscription and unsubscription. Unfortunately, using this approach with standard events requires that (1) any class which publishes the events to which one subscribes must have a thread-safe (preferably lock-free as well) 'remove' handler that can be safely called from the Finalize thread, and (2) all objects which are directly or indirectly referenced by the object that's used for event unsubscription will be kept semi-alive until the GC pass after the finalizer has run. Use of a different event paradigm (eg subscribing to an event by calling a function which returns an IDisposable which one may use to unsubscribe) may avoid these limitations.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM