簡體   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