.NET对象事件和处理/垃圾回收

10
编辑:在Joel Coehoorn的出色回答之后,我明白了我需要更具体地阐述问题,因此我修改了我的代码以更接近我想要理解的内容... 事件:据我所知,在后台,事件是“事件处理程序”或委托的“集合”,当事件被触发时,它们将被执行。因此,对我来说,如果对象Y有事件E,并且对象X订阅事件Y.E,那么Y将引用X,因为Y必须执行位于X中的方法,这样,X就不能被回收,这一点我理解了。
//Creates reference to this (b) in a.
a.EventHappened += new EventHandler(this.HandleEvent);

但这并不是Joel Coehoorn所说的内容...

然而,与事件相关的问题在于有时人们想要将IDisposable与具有事件的类型一起使用。问题在于,当类型X订阅另一种类型Y中的事件时,X现在具有对Y的引用。这个引用将防止Y被回收。

我不明白X如何引用Y

我稍微修改了一下我的示例,以更好地说明我的情况:

class Service //Let's say it's windows service that must be 24/7 online
{       
    A _a;

    void Start()
    {
       CustomNotificationSystem.OnEventRaised += new EventHandler(CustomNotificationSystemHandler)
       _a = new A();

       B b1 = new B(_a);
       B b2 = new B(_a);
       C c1 = new C(_a);
       C c2 = new C(_a);
    }

    void CustomNotificationSystemHandler(args)
    {

        //_a.Dispose(); ADDED BY **EDIT 2***
        a.Dispose();

        _a = new A();
        /*
        b1,b2,c1,c2 will continue to exists as is, and I know they will now subscribed
        to previous instance of _a, and it's OK by me, BUT in that example, now, nobody
        references the previous instance of _a (b not holds reference to _a) and by my
        theory, previous instance of _a, now may be collected...or I'm missing
        something???
        */
    }

}  

class A : IDisposable
        {
           public event EventHandler EventHappened;
        }

        class B
        {          
           public B(A a) //Class B does not stores reference to a internally.
           {
              a.EventHappened += new EventHandler(this.HandleEventB);
           }

           public void HandleEventB(object sender, EventArgs args)
           {
           }
        }

        class C
        {          
           public C(A a) //Class B not stores reference to a internally.
           {
              a.EventHappened += new EventHandler(this.HandleEventC);
           }

           public void HandleEventC(object sender, EventArgs args)
           {
           }
        }

编辑2: 好的,现在清楚了,当订阅者订阅发布者的事件时,它不会在订阅者中创建对发布者的引用。只有从发布者到订阅者的引用被创建(通过EventHandler)......在这种情况下,如果发布者在订阅者之前被GC收集(订阅者的生命周期大于发布者),那么就没有问题。

但是......据我所知,不能保证GC何时收集发布者,因此理论上,即使订阅者的生命周期大于发布者,仍可能发生订阅者可以进行收集,但发布者仍未被收集的情况(我不知道在最近的GC周期内,GC是否足够聪明,能够先收集发布者,然后再收集订阅者)。

无论如何,在这种情况下,由于我的订阅者没有直接引用到发布者并且无法取消订阅事件,因此我希望使发布者实现IDisposable,在删除所有对其的引用之前将其处理掉(请参见我的示例中的CustomNotificationSystemHandler)。

再次强调:我应该在发布者的Dispose方法中写些什么,以清除所有对订阅者的引用?应该是EventHappened -= null;还是EventHappened = null;或者没有办法以这种方式做到,我需要像下面一样做些什么?

public event EventHandler EventHappened
   {
      add 
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] + value;
      }
      remove
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] - value; 
      }
   }

可能是Remove handlers on disposing object的重复问题。 - Jason Down
这不是那个问题的副本。 - Justin
你的问题是什么?你进行了太多次编辑,所以我无法理解讨论的主题是什么。请在问题底部添加编辑内容,而不是在顶部,并且不要删除最初的问题。 - Mokhabadi
7个回答

14
对象B的生命周期比A长,所以A可能更早被处理。 听起来你把“处理”和“回收”搞混了? 释放对象与内存或垃圾回收没有任何关系。为了确保一切清楚,让我们分开这两个情况,然后我会在最后讲述事件:
回收: 无论你做什么,都不会使A在其父对象B之前被回收。只要B是可访问的,A也是可访问的。即使A是私有的,它仍然可以从B中的任何代码访问,因此只要B是可访问的,A就被认为是可访问的。这意味着垃圾回收器无法确定您是否已经完成了它的使用,并且永远不会收集A,直到也可以安全地收集B。即使显式调用GC.Collect()或类似方法,这也是正确的。只要一个对象是可访问的,它将不会被收集。
处理: 我甚至不确定为什么要在此实现IDisposable(它与内存或垃圾回收没有任何关系),但我暂且相信我们只是看不到非托管资源。你可以在任何时候释放A,没有任何限制。只需调用a.Dispose(),就完成了。.Net框架自动调用Dispose()的唯一方法是在using块的结尾处。除非您作为对象的终结器的一部分执行它,否则垃圾回收不会调用Dispose()(更多关于终结器的内容稍后讲述)。

实现IDisposable接口时,你向程序员传达了这个类型应该(甚至可以说是“必须”)及时释放。任何实现IDisposable的对象都有两种正确的模式(每种模式还有两种变化)。第一种模式是将类型本身包含在using块中。当无法使用此模式时(例如:代码如你所写,其中该类型是另一个类型的成员),第二种模式则是父类型也应该实现IDisposable,以便自己也可以被包括在using块中,并且其Dispose()方法可以调用你的类型的Dispose()方法。这些模式的变化之一是使用try/finally块,而不是using块,在finally块中调用Dispose()。

现在讲解终结器。唯一需要实现终结器的情况是IDisposable类型原始地产生非托管资源。例如,如果你上面的类型A只是包装像SqlConnection这样的类,它不需要终结器,因为SqlConnection本身的终结器将处理任何必要的清理工作。但是,如果你的类型A正在实现与全新的数据库引擎连接,则需要终结器来确保在收集对象时关闭连接。然而,你的类型B即使管理/包装你的类型A,也不需要终结器,因为类型A会负责完成连接的终结。

事件:

从技术上讲,事件仍然是托管代码,不应该需要释放。但是,对于带有事件的类型,有一个问题,有时人们喜欢使用IDisposable。问题在于当类型X订阅另一个类型Y中的事件时,Y现在具有对X的引用。这个引用可能会阻止X被收集。如果你期望Y的生命周期比X长,就会遇到问题,特别是如果Y相对于随时间而来和去的许多X具有非常长的寿命。

为解决这个问题,有时候程序员会让类型Y实现IDisposable接口,Dispose()方法的目的是取消订阅任何事件,以便订阅对象也能被回收。严格来说,这并不是IDisposable模式的目的,但它足够有效,我不想对此争论。使用此模式处理事件时需要了解两件事情:
  1. 如果实现IDisposable的唯一原因是处理事件,那么您无需定义finalizer。
  2. 仍然需要为您的类型的实例使用using或try/finally语句块,否则Dispose()方法将不会被调用,您的对象仍然无法被回收。
在这种情况下,类型A是类型B的内部类型,因此只有类型B可以订阅A的事件。由于'a'是类型B的成员变量,只有当B不再可达时,a和B都不再可达,事件订阅引用就不会计数。这意味着由A的事件持有B的引用不会阻止B被回收。然而,如果您在其他地方使用A类型,您可能仍然希望让A实现IDisposable接口以确保事件被正确取消订阅。如果这样做,请确保遵循整个模式,使A的实例被包含在using或try/finally代码块中。

1
我认为如果X订阅了Y的事件,那么Y会引用X,而不是相反。 - Luca
是的,我想我搞反了。可能需要更新,但我得等到工作放缓才能再仔细检查一下……也许要到今晚很晚了。 - Joel Coehoorn
如果Y发布了一个事件,并且X订阅了该事件,Y将通过MulticastDelegate获得对X的引用。但是,如果没有对Y的引用,X如何订阅Y的事件?这里有任何愚蠢的错误吗? - atiyar
1
@Nero,请查看我的问题编辑。X 在构造函数中接收了 Y,订阅了 Y 的事件,但是没有在内部存储对 Y 的引用。 - Alex Dn

5

我已经在你的样例代码中添加了我的评论。

class A : IDisposable
{
   public event EventHandler EventHappened
   {
      add 
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] + value;
      }
      remove
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] - value; 
      }
   }

   public void Dispose()
   {
      //Amit: If you have only one event 'EventHappened', 
      //you can clear up the subscribers as follows

      eventTable["EventHappened"] = null;

      //Amit: EventHappened = null will not work here as it is 
      //just a syntactical sugar to clear the compiler generated backing delegate.
      //Since you have added 'add' and 'remove' there is no compiler generated 
      //delegate to clear
      //
      //Above was just to explain the concept.
      //If eventTable is a dictionary of EventHandlers
      //You can simply call 'clear' on it.
      //This will work even if there are more events like EventHappened          
   }
}

class B
{          
   public B(A a)
   {
      a.EventHappened += new EventHandler(this.HandleEventB);

      //You are absolutely right here.
      //class B does not store any reference to A
      //Subscribing an event does not add any reference to publisher
      //Here all you are doing is calling 'Add' method of 'EventHappened'
      //passing it a delegate which holds a reference to B.
      //Hence there is a path from A to B but not reverse.
   }

   public void HandleEventB(object sender, EventArgs args)
   {
   }
}

class C
{          
   public C(A a)
   {
      a.EventHappened += new EventHandler(this.HandleEventC);
   }

   public void HandleEventC(object sender, EventArgs args)
   {
   }
}

class Service
{       
    A _a;

    void Start()
    {
       CustomNotificationSystem.OnEventRaised += new EventHandler(CustomNotificationSystemHandler)

       _a = new A();

       //Amit:You are right all these do not store any reference to _a
       B b1 = new B(_a);
       B b2 = new B(_a);
       C c1 = new C(_a);
       C c2 = new C(_a);
    }

    void CustomNotificationSystemHandler(args)
    {

        //Amit: You decide that _a has lived its life and must be disposed.
        //Here I assume you want to dispose so that it stops firing its events
        //More on this later
        _a.Dispose();

        //Amit: Now _a points to a brand new A and hence previous instance 
        //is eligible for collection since there are no active references to 
        //previous _a now
        _a = new A();
    }    
}

如上面代码中我的注释所解释的那样,您并没有错过任何东西 :)

但是...我知道,不能保证GC何时会收集发布者,因此从理论上讲,即使订阅者的生命周期大于发布者,也可能发生订阅者可以合法进行回收,但发布者仍未被收集的情况(我不知道在最近的GC周期内,GC是否足够聪明地首先收集发布者,然后再收集订阅者)。

由于发布者引用订阅者,因此订阅者在发布者之前成为可回收的永远不会发生,但反过来则可能是真的。如果发布者在订阅者之前被收集,那么就像您所说的那样,没有问题。如果订阅者属于比发布者低的GC代,则由于发布者持有对订阅者的引用,GC将把订阅者视为可达的并且不会将其收集。如果两者属于同一代,则它们将一起被收集。

由于我的订阅者没有对发布者进行直接引用并且无法取消订阅事件,因此我想让发布者实现IDisposable

与一些人建议的相反,如果您在任何时候确定对象不再需要,我建议实现dispose。仅更新对象引用可能并不总是导致对象停止发布事件。

考虑以下代码:

class MainClass
{
    public static Publisher Publisher;

    static void Main()
    {
        Publisher = new Publisher();

        Thread eventThread = new Thread(DoWork);
        eventThread.Start();

        Publisher.StartPublishing(); //Keep on firing events
    }

    static void DoWork()
    {
        var subscriber = new Subscriber();
        subscriber = null; 
        //Subscriber is referenced by publisher's SomeEvent only
        Thread.Sleep(200);
        //We have waited enough, we don't require the Publisher now
        Publisher = null;
        GC.Collect();
        //Even after GC.Collect, publisher is not collected even when we have set Publisher to null
        //This is because 'StartPublishing' method is under execution at this point of time
        //which means it is implicitly reachable from Main Thread's stack (through 'this' pointer)
        //This also means that subscriber remain alive
        //Even when we intended the Publisher to stop publishing, it will keep firing events due to somewhat 'hidden' reference to it from Main Thread!!!!
    }
}

internal class Publisher
{
    public void StartPublishing()
    {
        Thread.Sleep(100);
        InvokeSomeEvent(null);
        Thread.Sleep(100);
        InvokeSomeEvent(null);
        Thread.Sleep(100);
        InvokeSomeEvent(null);
        Thread.Sleep(100);
        InvokeSomeEvent(null);
    }

    public event EventHandler SomeEvent;

    public void InvokeSomeEvent(object e)
    {
        EventHandler handler = SomeEvent;
        if (handler != null)
        {
            handler(this, null);
        }
    }

    ~Publisher()
    {
        Console.WriteLine("I am never Printed");
    }
}

internal class Subscriber
{
    public Subscriber()
    {
        if(MainClass.Publisher != null)
        {
            MainClass.Publisher.SomeEvent += PublisherSomeEvent;
        }
    }

    void PublisherSomeEvent(object sender, EventArgs e)
    {
        if (MainClass.Publisher == null)
        {
            //How can null fire an event!!! Raise Exception
            throw new Exception("Booooooooommmm");
            //But notice 'sender' is not null
        }
    }
}

如果你运行以上代码,很可能会收到“Booooooooommmm”的消息。因此,事件发布者必须在确定其生命周期结束时停止触发事件。
这可以通过Dispose方法来实现。
有两种方法可以实现:
1.设置一个“IsDisposed”标志,并在触发任何事件之前检查它。
2.清除事件订阅者列表(如我在您的代码中建议的)。
2的好处是您释放了对订阅者的任何引用,从而使它们能够被收集(正如我之前解释的,即使发布者是垃圾,但属于更高的代,它仍然可能延长较低代的订阅者的收集时间)。
尽管,不可否认,由于发布者的“隐藏”可达性,你很少会遇到演示的行为,但正如你所看到的,2的好处是明显的,并且适用于所有事件发布者,特别是长期存活的发布者(单例模式!!!)。这本身就值得实现Dispose并采用2。

谢谢!你已经让我对事件的几乎所有事情都很清楚了 :) 但是如果我在HashTable中使用add/remove和manage handlers,我该如何触发它们?通常我会这样做:if(event!=null)event(..)。如何将事件“映射”到HashTable中正确的处理程序? - Alex Dn
EventHandler订阅者 = (EventHandler)eventTable["EventHappened"]; 如果(订阅者 != null) 订阅者(this, EventArgs.Empty); - Amit Mittal
在从字典中检索订阅者之前,您应该先检查键“EventHappened”是否存在。顺便说一下,仅当您的类公开大量事件但只有少数事件在任何时候被订阅(例如WinForms控件)时,管理EventHandlers字典才具有优势,因为让编译器为每个事件生成默认的后备委托将导致类对象浪费内存,而实际上它并不需要所有那些内存。 - Amit Mittal

4
与其他答案所声称的相反,其发布者的GC生命周期可能超过订阅者有用生命周期的事件应被视为非托管资源。短语“非托管资源”中的术语“非托管”并不意味着“完全在托管代码世界之外”,而是涉及对象是否需要清除超出受管理垃圾回收器提供的范围。
例如,集合可能会公开CollectionChanged事件。如果某些其他类型的对象订阅此类事件并反复创建和放弃,那么集合可能会保持对每个这样的对象的委托引用。如果这种创建和放弃每秒发生一次(如果所讨论的对象是在更新UI窗口的例程中创建,则可能会发生这种情况),则这些引用的数量可能会每天增加超过86,000。对于从未运行超过几分钟的程序来说并不是什么大问题,但对于可以连续运行数周的程序来说绝对是致命的。
Microsoft没有在vb.net或C#中想出更好的事件清理模式真的很不幸。实际上,一个订阅事件的类实例在被放弃之前没有任何理由不清理它们,但是微软没有采取任何措施来促进这种清理。实际上,人们可以在足够频繁地放弃订阅事件的对象的情况下逃脱(因为事件发布者将在与订阅者相同的时间内超出范围),以至于确保事件得到正确清理所需的烦人程度似乎不值得。不幸的是,不总是容易预测事件发布者可能比预期生存更长的所有情况;如果许多类留下悬挂的事件,则由于其中一个事件订阅属于长寿命对象,可能存在大量无法收集的内存。
针对编辑的补充说明
如果XY订阅事件,然后放弃了所有对Y的引用,并且Y变得可回收,那么X将不会阻止Y被回收。这是一件好事情。如果X保持对Y的强引用,以便能够处理它,这样的引用将防止Y被回收。这可能不是一件好事。在某些情况下,最好为X保留一个长时间的WeakReference(使用第二个参数设置为true的构造函数),而不是直接引用Y。如果XDispose时,WeakReference的目标非空,它将必须取消订阅Y的事件。如果目标为空,无法取消订阅,但这并不重要,因为到那时,Y(及其对X的引用)将完全不存在。请注意,在Y死亡并复活的极少数情况下,X仍将希望取消订阅其事件;使用长时间的WeakReference将确保仍然可以执行此操作。
虽然有人认为X不必保留对Y的引用,而Y应该简单地编写使用某种弱事件分派的代码,但在一般情况下,这种行为是不正确的,因为没有办法让Y知道X会做任何其他代码可能关心的事情,即使Y持有对X的唯一引用。完全有可能X持有对某个强根对象的引用,并且在其事件处理程序内对该其他对象执行某些操作。Y持有对X的唯一引用并不意味着没有其他对象对X“感兴趣”。唯一通用的解决方案是,不再对其他对象的事件感兴趣的对象通知后者对象该事实。

1

我会让我的B类也实现IDisposable接口,在它的dispose方法中,我会首先检查A是否为null,然后再释放A。通过使用这种方法,您只需要确保最后一个类被处理,内部将处理所有其他dispose。


1

MSDN 参考

为了防止事件被触发时调用事件处理程序,应该取消订阅事件。为了防止资源泄漏,在释放订阅对象之前,应该先取消订阅事件。在取消订阅事件之前,发布对象中潜在的多路广播委托会引用封装订阅者事件处理程序的委托。只要发布对象保持该引用,垃圾回收就不会删除您的订阅对象。

当所有订阅者都从事件中取消订阅后,发布类中的事件实例将设置为 null。


问题在于我没有处理订阅者,而是处理了发布者(订阅者仍然存在),此外订阅者在内部没有对发布者的引用。 - Alex Dn

0

在处理对象时,您不需要取消挂钩事件处理程序,尽管您可能希望这样做。我的意思是,GC会很好地清理事件处理程序,而无需您的任何干预,但是根据情况,您可能希望在GC执行之前删除这些事件处理程序,以防止在您不希望调用处理程序时调用它们。

在您的示例中,我认为您颠倒了角色 - 类A实际上不应该取消其他人添加的事件处理程序,并且也没有真正需要删除事件处理程序,因为它可以停止引发那些事件!

但是,假设情况被颠倒了

class A
{
   public EventHandler EventHappened;
}

class B : IDisposable
{
    A _a;
    private bool disposed;

    public B(A a)
    {
        _a = a;
        a.EventHappened += this.HandleEvent;
    }

    public void Dispose(bool disposing)
    {
        // As an aside - if disposing is false then we are being called during 
        // finalization and so cannot safely reference _a as it may have already 
        // been GCd
        // In this situation we dont to remove the handler anyway as its about
        // to be cleaned up by the GC anyway
        if (disposing)
        {
            // You may wish to unsubscribe from events here
            _a.EventHappened -= this.HandleEvent;
            disposed = true;
        }
    }

    public void HandleEvent(object sender, EventArgs args)
    {
        if (disposed)
        {
            throw new ObjectDisposedException();
        }
    }
 }

如果在B被处理后,A仍然可以继续引发事件,并且B的事件处理程序可能会执行某些操作,如果B被处理,则可能会导致异常或其他意外行为,那么最好先取消订阅此事件。

不,我没有反转角色,我修改了我的问题以使其更清晰。 - Alex Dn

0

对象A通过EventHandler委托引用B(A具有一个引用B的EventHandler实例)。 B没有任何对A的引用。当将A设置为null时,它将被收集并释放内存。因此,在这种情况下,您不需要清除任何内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接