简体   繁体   English

使用WeakReference解决.NET未注册事件处理程序导致内存泄漏的问题

[英]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. 如果该实例未能注销事件处理程序(通过Dispose,可能),那么垃圾收集器将不会释放实例内存。

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. 如果我实例化一个Foo,并将其传递给一个新的Bar构造函数,那么放开Bar对象,由于AnEvent注册,垃圾收集器不会释放它。

I consider this a memory leak, and seems just like my old C++ days. 我认为这是一个内存泄漏,看起来就像我的旧C ++时代。 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? 当然,我可以使Bar IDisposable,在Dispose()方法中取消注册事件,并确保在它的实例上调用Dispose(),但为什么我必须这样做?

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. 我看过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). 它不仅使用起来非常困难,而且其文档也不充分(请参阅http://msdn.microsoft.com/en-us/library/system.windows.weakeventmanager.aspx - 注意到“对继承者的说明”部分,有6个模糊的子弹)。

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. 我提出了一个基于WeakReference的简单解决方案,如此处所述。 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; 注意两件事:1。Foo类以两种方式修改:事件被WeakReferenceEvent实例替换,如下所示; and the invocation of the event is changed. 并且更改了事件的调用。 2. The Bar class is UNCHANGED. 2. Bar类是UNCHANGED。

No need to subclass WeakEventManager, implement IWeakEventListener, etc. 无需子类WeakEventManager,实现IWeakEventListener等。

OK, so on to the implementation of WeakReferenceEvent. 好的,等等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 请注意,它使用我从这里借来的通用WeakReference <T>: 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. 我重写operator +来获取+ =语法糖匹配事件。 This creates WeakReferences to the Action delegate. 这会为Action委托创建WeakReferences。 This allows the garbage collector to free the event target object (Bar in this example) when nobody else is holding on to it. 这允许垃圾收集器在没有其他人持有时释放事件目标对象(在此示例中为Bar)。

In the Invoke() method, simply run through the weak references and call their Target Action. 在Invoke()方法中,只需运行弱引用并调用其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. 当然,这仅适用于Action类型的委托。 I tried making this generic, but ran into the missing where T : delegate in C#! 我尝试制作这个通用的,但遇到了丢失的地方T:委托在C#!

As an alternative, simply modify class WeakReferenceEvent to be a WeakReferenceEvent<T>, and replace the Action with Action<T>. 作为替代方案,只需将类WeakReferenceEvent修改为WeakReferenceEvent <T>,并将Action替换为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: 带有<T>的完整代码和运算符 - (用于删除事件)如下所示:

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. 是的,的确,我错过了一个小细节:调用+ =来注册事件(l.AnEvent + = l_AnEvent;)会创建一个隐式的Action对象。 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. 因此,当调用返回并且垃圾收集器运行时,将释放隐式创建的Action对象(现在只有弱引用指向它),并且事件未注册。

A (painful) solution is to hold a reference to the Action object as follows: 一个(痛苦的)解决方案是保存对Action对象的引用,如下所示:

    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 所以简而言之......你使用强有力的参考有两个原因...... 1.类型安全(这里不太适用)2。性能

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? 上次我看到那个论点摆到桌面上但是海报看起来是生成的msil预先录制的,也许这可以让你对这个问题有所启发?

One other thought though ... What if you attached an event handler to say a com object event? 另一个想法...如果你附加一个事件处理程序来说一个com对象事件怎么办? 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) (foo的一个例子)
  2. The object that represents your event handler. 表示事件处理程序的对象。 (For example, an instance of EventHandler or an instance of Action.) (例如,EventHandler的一个实例或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. 您认为存在内存泄漏的原因是EventHandler(或Action)对象在内部持有对Foo对象的引用。 This will prevent your Foo object from being collected. 这将阻止收集您的Foo对象。

Now, why can't you write a WeakEventHandler? 现在,为什么不能写一个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 您的委托(EventHandler或Action的实例)永远不会对您的Foo对象进行硬引用
  2. Your WeakEventHandler holds a strong reference to your delegate, and a weak reference to your Foo object 您的WeakEventHandler拥有对您的委托的强引用,以及对您的Foo对象的弱引用
  3. You have provisions to eventually unregister your WeakEventHandler when the weak reference becomes invalid. 当弱引用变为无效时,您有规定最终取消注册WeakEventHandler。 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. 您的WeakEventHandler和Action对象很有可能进入Gen 1或Gen 2.这将导致垃圾收集器的高负载。
  • WeakReferences hold a GC Handle. WeakReferences持有GC句柄。 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"; 另一种方法是给对象实际感兴趣的每个人一个对包装器的引用,而包装器又包含对“guts”的引用; the event handler only has a reference to the "guts", and the guts do not hold a strong reference to the wrapper. 事件处理程序只引用了“guts”,而guts没有对包装器的强引用。 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). 包装器还包含对一个对象的引用,该对象的Finalize方法将取消订阅该事件(最简单的方法是使用一个简单的类,其Finalize方法调用一个值为False的Action <Boolean>,并且其Dispose方法调用该方法委托值为True并禁止最终化)。

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. 这种方法的缺点是要求主类上的所有非事件操作都通过包装器(一个额外的强引用)来完成,但是避免必须将任何WeakReferences用于除事件订阅和取消订阅之外的任何其他操作。 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. 不幸的是,对标准事件使用这种方法需要(1)任何发布一个订阅的事件的类必须具有线程安全(最好是无锁)的'remove'处理程序,可以从Finalize线程安全地调用,和(2)用于事件取消订阅的对象直接或间接引用的所有对象将保持半活动状态,直到终结器运行后GC通过。 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. 使用不同的事件范例(例如,通过调用返回可用于取消订阅的IDisposable的函数来订阅事件)可以避免这些限制。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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