【问题标题】:.NET object events and dispose / GC.NET 对象事件和处置/GC
【发布时间】:2012-07-16 15:37:43
【问题描述】:

编辑:在 Joel Coehoorns 出色的回答之后,我明白我需要更具体,所以我修改了我的代码以更接近我想要理解的东西......

事件: 据我了解,在后台,事件是 EventHandlers 又名委托的“集合”,当事件引发时将被执行。所以对我来说,这意味着如果对象 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 中)。

再一次 我应该在发布者处理方法中写什么以清除对订阅者的所有引用?应该是 EventHappened -= null;或 EventHappened = null;或者没有办法以这种方式做到这一点,我需要做类似下面的东西???

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

【问题讨论】:

  • 这不是那个问题的重复。
  • 问题是什么?你有太多的编辑,所以我不明白讨论的主题是什么。请在不在顶部的问题按钮上添加编辑,并且不要删除初始问题。

标签: c# .net events garbage-collection dispose


【解决方案1】:

对象 B 的生命周期比 A 长,所以 A 可能被更早地处理掉

听起来您将“处置”与“收集”混淆了?处置对象与内存或垃圾回收无关。为了确保一切都清楚,让我们分解两个场景,然后我将继续讨论最后的事件:

收藏:

没有您所做的任何事情都不会允许 A 在其父 B 之前被收集。只要 B 可访问,A 也是如此。即使 A 是私有的,它也是仍然可以从 B 中的任何代码访问,因此只要 B 是可达的,A 就被认为是可达的。这意味着垃圾收集器不确定您是否已经完成了它,并且在收集 B 也是安全的之前永远不会收集 A。即使您明确调用 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() 方法的目的是取消订阅任何事件,以便也可以收集订阅对象。从技术上讲,这不是 Dispose() 模式的目的,但它工作得很好,我不打算讨论它。将此模式与事件一起使用时,您需要了解两件事:

  1. 如果这是实现 IDisposable 的唯一原因,则不需要终结器
  2. 您的类型的实例仍然需要 using 或 try/finally 块,或者您没有获得任何东西。否则 Dispose() 将不会被调用,并且您的对象仍然无法被收集。

在这种情况下,您的类型 A 对类型 B 是私有的,因此只有类型 B 可以订阅 A 的事件。由于 'a' 是类型 B 的成员,因此在 B 不再可访问之前,两者都没有资格进行垃圾收集,此时两者都将不再可访问并且事件订阅引用将不计算在内。这意味着 A 的事件在 B 上持有的引用不会阻止 B 被收集。但是,如果您在其他地方使用 A 类型,您可能仍希望 A 实现 IDisposable 以确保您的事件被取消订阅。如果您这样做,请确保遵循整个模式,以便将 A 的实例包含在 using 或 try/finally 块中。

【讨论】:

  • 我认为如果 X 订阅 Y 的事件,Y 会引用 X,而不是反之亦然。
  • 是的,我想我把它倒过来了。可能需要更新,但我必须等到工作放慢时再进行审查……也许今晚很晚。
  • 如果 Y 发布了一个事件,而 X 订阅了该事件,Y 获得了对 X 的引用(通过后面的 MulticastDelegate)。但是没有对 Y 的引用,X 怎么订阅 Y 的事件呢?我在这里犯了什么愚蠢的错误吗?
  • @Nero 在我的问题编辑中看到。 X 在构造函数中收到 Y,订阅 Y 的事件,然后在内部不存储对 Y 的引用。
【解决方案2】:

我已在您的示例代码中添加了 My cmets。

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

b1,b2,c1,c2 将继续存在,我知道他们现在会 订阅了 _a 的前一个实例,我没关系,但在那 例如,现在,没有人引用 _a 的前一个实例(b 不是 持有对 _a) 的引用,根据我的理论,之前的 _a 实例,现在 可能会被收集...或者我错过了什么???

正如我在上面的代码中通过我的 cmets 所解释的,你在这里没有遗漏任何东西 :)

但是...据我所知,不能保证 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
        }
    }
}

如果您运行上述代码,您通常会收到“Boooooooooommmm”。因此,我们的想法是事件发布者必须在我们确定其生命周期结束时停止触发事件。

这可以通过 Dispose 方法完成。

有两种方法可以实现:

  1. 设置标志“IsDisposed”并在触发任何事件之前对其进行检查。
  2. 清除事件订阅者列表(按照我的 cmets 在您的代码中的建议)。

2 的好处是您释放对订阅者的任何引用,从而启用那里的收集(正如我之前解释的那样,即使发布者是垃圾但属于更高代,那么它仍可能会延长对较低代订阅者的收集)。

虽然,诚然,由于发布者的“隐藏”可达性,您很少会遇到所展示的行为,但正如您所见,2 的好处是显而易见的,并且对所有事件发布者都有效,尤其是长期存在的发布者(单身人士任何人!!!)。这本身就值得实现 Dispose 并使用 2。

【讨论】:

  • 谢谢!您对我来说几乎所有关于事件的事情都清楚:) 但是如果我在 HashTable 中使用添加/删除和管理处理程序,我如何提高它们?我经常做 if(event!=null)event(..)。如何将事件“映射”到哈希表中的正确处理程序?
  • EventHandler 订阅者 = (EventHandler)eventTable["EventHappened"]; if(subscribers != null) 订阅者(this,EventArgs.Empty);
  • 一点更正,在从字典中检索订阅者之前,您应该检查键“EventHappened”是否存在。顺便说一句,只有当您的类公开大量事件时,管理 EventHandlers 的字典才有优势,而在任何时候(如 WinForms 控件)预计只有少数事件被订阅,因为让编译器生成默认支持每个事件的委托都会导致类对象的内存浪费,而实际上它并不需要所有的内存。
【解决方案3】:

与其他一些答案声称的相反,发布者的 GC 生命周期可能超过订阅者的有用生命周期的事件应被视为非托管资源。短语“非托管资源”中的“非托管”一词并不意味着“完全在托管代码的世界之外”,而是与对象是否需要在托管垃圾收集器提供的清理之外进行清理有关。

例如,一个集合可能会暴露一个CollectionChanged 事件。如果订阅此类事件的某些其他类型的对象被重复创建和放弃,则集合可能最终持有对每个此类对象的委托引用。如果发生这种创建和放弃,例如每秒一次(如果有问题的对象是在更新 UI 窗口的例程中创建的,则可能会发生这种情况)在程序运行的每一天,此类引用的数量可能会增加超过 86,000 个。对于一个从不运行超过几分钟的程序来说不是什么大问题,但对于一个可以一次运行数周的程序来说绝对是个杀手。

很遗憾微软没有在 vb.net 或 C# 中提出更好的事件清理模式。很少有任何理由说明订阅事件的类实例在被放弃之前不应该清理它们,但微软没有采取任何措施来促进这种清理。在实践中,放弃订阅事件足够频繁的对象(因为事件发布者将与订阅者大约在同一时间超出范围)可以逃脱惩罚,确保事件得到正确清理所需的烦人程度的努力并没有似乎不值得。不幸的是,预测事件发布者的寿命可能比预期长的所有情况并不总是那么容易。如果许多类使事件悬空,则有可能无法收集大量内存,因为其中一个事件订阅恰好属于一个长期存在的对象。

针对编辑的附录

如果X 订阅来自Y 的事件,然后放弃对Y 的所有引用,并且如果Y 符合收集条件,X 不会阻止Y 被收集。那将是一件好事。如果X 保留对Y 的强引用以便能够处理它,那么这样的引用将阻止Y 被收集。这可能不是一件好事。在某些情况下,X 与Y 保持一个较长的WeakReference(第二个参数设置为true)比直接引用更好;如果X 为Disposed 时WeakReference 的目标不为空,则必须取消订阅Y 的事件。如果目标为空,它不能取消订阅,但没关系,因为到那时Y(以及它对X 的引用)将完全不存在。请注意,万一Y 死了又复活了,X 仍然想取消订阅它的事件;使用长 WeakReference 将确保这种情况仍然会发生。

虽然有些人认为 X 不应该保留对 Y 的引用,而 Y 应该简单地编写为使用某种弱事件调度,但这种行为在一般情况下是不正确的,因为Y 无法判断X 是否会做任何其他代码可能关心的事情即使Y 拥有对X 的唯一引用。 X 完全有可能持有对某个强根对象的引用,并可能在其事件处理程序中对其他对象执行某些操作。 Y 拥有对X 的唯一引用这一事实不应暗示没有其他对象对X “感兴趣”。唯一普遍正确的解决方案是让不再对其他对象的事件感兴趣的对象通知后面的对象该事实。

【讨论】:

    【解决方案4】:

    我会让我的 B 类也实现 IDisposable,并且在它的处置例程中,我将首先检查 A 是否不为空,然后处置 A。通过使用这种方法,您必须确保处置最后一个类内部将处理所有其他处置。

    【讨论】:

      【解决方案5】:

      MSDN Reference

      “为防止在引发事件时调用您的事件处理程序,请取消订阅该事件。为了防止资源泄漏,您应该在处理订阅者对象之前取消订阅事件。直到您取消订阅事件,发布对象中的事件基础的多播委托具有对封装订阅者事件处理程序的委托的引用。只要发布对象持有该引用,垃圾回收就不会删除您的订阅者对象。"

      “当所有订阅者都取消订阅一个事件时,发布者类中的事件实例设置为空。”

      【讨论】:

      • 问题是我没有处理订阅者,我处理了发布者(订阅者继续存在),另外订阅者内部没有对发布者的引用。
      【解决方案6】:

      您需要在处理对象时取消挂钩事件处理程序,尽管您可能想要这样做。我的意思是,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();
              }
          }
       }
      

      如果A 有可能在B 已被释放后继续引发事件,并且B 的事件处理程序可能会执行一些可能导致异常或其他一些意外行为的事情,如果B 是那么首先取消订阅这个事件可能是个好主意。

      【讨论】:

      • 不,我没有反转卷,我修改了我的问题更清楚。
      【解决方案7】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-12-26
        • 2012-04-30
        • 2012-06-11
        • 2013-05-08
        • 2016-09-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多