繁体   English   中英

.NET对象事件和dispose / GC

[英].NET object events and dispose / GC

编辑:在Joel Coehoorns的优秀答案之后,我明白我需要更加具体,所以我修改了我的代码,使其更贴近我正在努力理解的事情......

事件:据我所知,在后台,事件是EventHandlers又名代表的“集合”,将在事件引发时执行。 所以对我来说,这意味着如果对象Y有事件E而对象X订阅了事件YE ,那么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将足够聪明,首先收集发布者,然后收集订阅者。

无论如何,在这种情况下,由于我的订阅者没有直接引用发布者并且无法取消订阅该事件,我想让发布者实现IDisposable,以便在删除所有对他的引用之前将其处理掉(参见CustomNotificationSystemHandler in我的例子)。

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

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

物体B的寿命比A长,因此A可以更早地处理

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

采集:

不管你做什么都不会允许一个要收集它的父B.只要B是到达之前,所以是A.即使是私有的,它仍然是可到达的任何代码B内部,所以只要B是可达的, A被认为是可达的。 这意味着垃圾收集器不确定您是否完成了它,并且在收集B之前永远不会收集A.即使您明确调用GC.Collect()或类似的东西也是如此。 只要对象可以访问,就不会收集它。

处理:

我甚至不确定你为什么要在这里实现IDisposable(它与内存或垃圾收集无关 ),但是我会给你带来怀疑的好处,因为我们只是看不到非托管资源。

没有什么能阻止您随时处置A。 只需调用a.Dispose(),就完成了。 .Net框架将自动为您调用Dispose()的唯一方法是using块结束。 在垃圾收集期间不会调用Dispose(),除非您将其作为对象的终结器的一部分(稍后更多关于终结器)。

在实现IDisposable时,您正在向程序员发送一条消息,即此类型应该(甚至可能“必须”)立即处理。 任何IDisposable对象都有两种正确的模式(模式有两种变体)。 第一种模式是将类型本身封装在一个使用块中。 当这是不可能的时(例如:类型是其他类型的成员的代码),第二种模式是父类型也应该实现IDisposable,因此它本身可以包含在一个使用块中,它是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. 你的类型的实例仍然需要使用或尝试/终止块,或者你没有获得任何东西。 否则,将不会调用Dispose(),仍然无法收集您的对象。

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

我在您的示例代码中添加了我的评论。

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实例,现在可能会被收集......或者我错过了什么?

正如我在上面的代码中的评论所解释的那样,你在这里没有遗漏任何东西:)

但是......据我所知,当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的好处是明确的,并且对所有活动出版商尤其是长寿的出版商都有效(Singletons任何人! !)。 这本身就值得实现Dispose并使用2。

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

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

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

回复编辑的补遗

如果X要从Y订阅一个事件然后放弃对Y所有引用,并且如果Y资格收集,则X不会阻止Y被收集。 那将是一件好事。 如果X为了能够处理它而对Y保持强烈的引用,这样的引用将阻止Y被收集。 这可能说不是一件好事。 在某些情况下, X最好将长WeakReference (一个用第二个参数设置为true构造)保持为Y而不是直接引用; 如果当XDispose d时WeakReference的目标为非null,则必须取消订阅Y的事件。 如果目标为空,则无法取消订阅,但无关紧要,因为那时Y (及其对X引用)将完全不存在。 请注意,在Y死亡并复活的不太可能的情况下, X仍然希望取消订阅其事件; 使用长WeakReference将确保仍然可以发生。

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

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

处理对象时,您不需要取消挂钩事件处理程序,尽管您可能需要 我的意思是,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布置那么它可能是一个好主意,从这个退订事件第一。

MSDN参考

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

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

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

暂无
暂无

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

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