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

时间:2010-05-11 22:33:23

标签: c# .net events memory-leaks garbage-collection

问题:已注册的事件处理程序创建从事件到事件处理程序实例的引用。如果该实例未能注销事件处理程序(通过Dispose,可能),那么垃圾收集器将不会释放实例内存。

示例:

    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()
        {

        }            
    }

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

我认为这是一个内存泄漏,看起来就像我旧的C ++时代。当然,我可以使Bar IDisposable,在Dispose()方法中取消注册事件,并确保在它的实例上调用Dispose(),但为什么我必须这样做?

我首先质疑为什么事件是通过强引用来实现的?为什么不使用弱引用?事件用于抽象地通知对象另一个对象的更改。在我看来,如果事件处理程序的实例不再使用(即,没有对该对象的非事件引用),那么它注册的任何事件都应该自动取消注册。我错过了什么?

我看过WeakEventManager。哇,多么痛苦。它不仅使用起来非常困难,而且它的文档也不充分(参见http://msdn.microsoft.com/en-us/library/system.windows.weakeventmanager.aspx - 注意到“继承人注释”部分有6个模糊的子弹)。

我在不同地方看过其他讨论,但我觉得我无法使用。我提出了一个基于WeakReference的简单解决方案,如此处所述。我的问题是:这是否不符合要求而且复杂程度要低得多?

要使用该解决方案,上述代码修改如下:

    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()
        {

        }
    }

注意两件事: 1. Foo类以两种方式修改:事件被WeakReferenceEvent实例替换,如下所示;并且更改了事件的调用。 2. Bar类是UNCHANGED。

无需子类WeakEventManager,实现IWeakEventListener等。

好的,等等WeakReferenceEvent的实现。这在这里显示。注意,它使用通用的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);
    }
}

它的功能是微不足道的。我重写operator +来获得+ =语法糖匹配事件。这会为Action委托创建WeakReferences。这允许垃圾收集器在没有其他人持有时释放事件目标对象(在本例中为Bar)。

在Invoke()方法中,只需运行弱引用并调用其目标操作。如果找到任何死亡(即垃圾收集)引用,请从列表中删除它们。

当然,这仅适用于Action类型的委托。我尝试制作这个通用的,但遇到了丢失的地方T:委托在C#!

作为替代方案,只需将类WeakReferenceEvent修改为WeakReferenceEvent&lt; T&gt;,并将Action替换为Action&lt; T&gt;。修复编译器错误,你有一个可以这样使用的类:

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

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

带有&lt; T&gt;的完整代码和运算符 - (用于删除事件)如下所示:

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

希望这会帮助其他人,当他们遇到神秘事件导致垃圾收集世界中的内存泄漏时!

4 个答案:

答案 0 :(得分:2)

我找到了我的问题的答案,为什么这不起作用。是的,的确,我错过了一个小细节:调用+ =来注册事件(l.AnEvent + = l_AnEvent;)会创建一个隐式的Action对象。该对象通常仅由事件本身(以及调用函数的堆栈)保存。因此,当调用返回并且垃圾回收器运行时,将释放隐式创建的Action对象(现在只有弱引用指向它),并且该事件未注册。

一个(痛苦的)解决方案是按如下方式保存对Action对象的引用:

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

这有效,但删除了解决方案的简单性。

答案 1 :(得分:2)

肯定会对性能产生影响。

有点像,当我可以使用反射动态读取程序集并在其类型中进行相关调用时,为什么在我的解决方案中引用其他程序集?

所以简而言之...... 你使用强引用有两个原因......      1.类型安全(这里不太适用)      2.表现

这可以追溯到关于哈希表的泛型的类似争论。 上次我看到那个论点摆到桌面上但是海报看起来是生成的msil pre-jitted,也许这可以帮助你了解这个问题?

另一个想法虽然...... 如果您附加了一个事件处理程序来说出一个com对象事件怎么办? 从技术上讲,该对象不受管理,所以它如何知道何时需要清理,当然这归结为框架如何处理范围?

这篇文章附带“它在我头脑中保证”,对于如何描述这篇文章不承担任何责任:)

答案 2 :(得分:0)

我知道有两种模式用于制作弱事件订阅:一种是让事件订阅者对指向他的委托持有强引用,而发布者持有对该委托的弱引用。这样做的缺点是需要通过弱引用来完成所有事件发射;它可能会向出版商发出任何事件是否已过期的通知。

另一种方法是给对象实际感兴趣的每个人一个对包装器的引用,而包装器又包含对“guts”的引用;事件处理程序只引用了“guts”,而guts没有对包装器的强引用。包装器还包含对一个对象的引用,该对象的Finalize方法将取消订阅该事件(最简单的方法是使用一个简单的类,其Finalize方法调用一个值为False的Action&lt; Boolean&gt;,并且其Dispose方法调用该方法委托值为True并禁止最终确定。)

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

答案 3 :(得分:0)

当你有一个事件处理程序时,你有两个对象:

  1. 你班级的目标。 (foo的一个例子)
  2. 表示事件处理程序的对象。 (例如,EventHandler的一个实例或Action的一个实例。)
  3. 您认为存在内存泄漏的原因是EventHandler(或Action)对象内部保存对Foo对象的引用。这样可以防止收集你的Foo对象。

    现在,为什么不能写一个WeakEventHandler?答案是你可以,但你基本上必须确保:

    1. 您的委托(EventHandler或Action的实例)永远不会对您的Foo对象进行硬引用
    2. 您的WeakEventHandler拥有对您的委托的强引用,以及对您的Foo对象的弱引用
    3. 当弱引用变为无效时,您有最终取消注册WeakEventHandler的条款。这是因为无法知道何时收集对象。
    4. 在实践中,这不值得。这是因为你有权衡:

      • 您的事件处理程序方法需要是静态的,并将对象作为参数,这样它就不会对您想要收集的对象进行强引用。
      • 您的WeakEventHandler和Action对象很有可能进入Gen 1或Gen 2.这将导致垃圾收集器负载过高。
      • WeakReferences持有GC句柄。这可能会对绩效产生负面影响。

      因此,确保正确取消注册事件处理程序是一种更好的解决方案。语法更简单,内存使用更好,应用程序性能更好。