Laden...

Timer und Dispose

Erstellt von Kuehner vor 14 Jahren Letzter Beitrag vor 14 Jahren 6.946 Views
K
Kuehner Themenstarter:in
489 Beiträge seit 2006
vor 14 Jahren
Timer und Dispose

Hallo,

Wenn ich die Dispose()-Funktion des Timers aufrufe, stoppt dann auch der Timer auch oder wird die Timer-Funktion trotzdem noch aufgerufen?

D
27 Beiträge seit 2009
vor 14 Jahren

Was hindert dich daran es auszuprobieren ? 😃

K
Kuehner Themenstarter:in
489 Beiträge seit 2006
vor 14 Jahren

Bin grad nicht bei der Arbeit und das Problem geht mir grad so im Kopf rum... nichts für Ungut. Falls es zufällig jemand weiß...

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo Kuehner,

ich würde mich wundern, wenn der Test zu einem anderen Ergebnis kommen würde, als was man vermuten würde (dispose = zerstören).

herbivore

PS: Deine Frage ist ja ungefähr: "Fährt ein Auto noch, nachdem ich es (endgültig) zerstört habe?"

3.971 Beiträge seit 2006
vor 14 Jahren

Bei Timern ist es nicht nur wegen der sofortigen Ressourcenfreigabe wichtig Dispose aufzurufen, sondern auch damit der GC diesen Timer nicht vorzeitig wegräumt.
Beispiel:


public static void Main() {
  //Führe alle x-Sekunden eine bestimmte Funktion aus, bis eine Taste gedrückt wird
  Timer tmr = new Timer(...);
  Console.ReadKey();
  tmr.Dispose();
}

Ohne Dispose würde der GC beim nächsten Collect auch fälschlicherweise die Tmr-Instanz wegräumen, da der GC erkennt, dass die Variable nicht mehr gebraucht wird. (Ein setzen auf null würde auch nicht helfen!)

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo kleines_eichhoernchen,

wobei man nicht zu einem "Trick" greifen muss, um das Aufräumen zu verhindern, sondern dies auch explizit tun kann: CG.KeepAlive.

herbivore

3.971 Beiträge seit 2006
vor 14 Jahren

Persönlich find ich ist in diesem Anwendungsfall GC.KeepAlive nicht der Hit. Wenn eine Klasse schon Dispose anbietet sollte diese auch aufgerufen werden, zumal man sich mit Using oder eine (Instanz) Klassenvariable auch behelfen kann.

Bei Klassen die kein Dispose, Close oder eine ähnliche Funktion besitzen, kann man KeepAlive benutzen oder alternativ auch eine statische Klassenvariable.

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo kleines_eichhoernchen,

Dispose sollte in der Tat so früh wie möglich aufgerufen werden. Wenn wir aber wie hier über einen Timer reden, der (potentiell) die gesamte Laufzeit der Anwendung über existieren soll, ist der eigentliche Sinn, den ein expliziter Aufruf von Dispose entfaltet, nicht gegeben. Wenn Dispose richtig implementiert wird, wird Dispose aus dem Destruktor automatisch aufgerufen.

Es spricht aus meiner Sicht einiges dafür, die Tatsache, dass der Timer weiter leben soll, auch wenn er (von außen) nicht benutzt wird (gerade weil er aus sich selbst heraus aktiv ist), explizit zum Ausdruck zu bringen.

Andersherum kann man sich natürlich fragen, warum ein (aktiver) Timer sich nicht intern selbst auf KeepAlive setzt. Das wäre eigentlich die beste Lösung, aber die kann nur Microsoft realisieren.

herbivore

G
40 Beiträge seit 2005
vor 14 Jahren

Hallo

Die Geschichte mit dem Timer und der Dispose - Methode, die man am Schluss aufrufen soll, um sicherzugehen, dass der Timer vom GC nicht vorzeitig abgeräumt wird, habe ich, soweit ich mich erinnere, auch in dem schönen Buch von Jeffrey Richter: " Microsoft.net Framework-Programmieung in C#" (Microsoft Press) gelesen. Leider finde ich die Seite nicht mehr, auf der das stand.
Jedenfalls habe ich, um sicherzugehen, dass während der Laufzeit einer Form die von ihr aufgerufenen Objekte auch bis zum Entladen der Form existieren, einer ganzen Reihe von Objekten Dispose spendiert, das dann letztlich von Form.Dispose aufgerufen wird.
Bisher war ich mir nie sicher, ob das wirklich erforderlich ist.

Jürgen

S
18 Beiträge seit 2009
vor 14 Jahren

Bei Timern ist es nicht nur wegen der sofortigen Ressourcenfreigabe wichtig Dispose aufzurufen, sondern auch damit der GC diesen Timer nicht vorzeitig wegräumt.
Beispiel:

  
public static void Main() {  
  //Führe alle x-Sekunden eine bestimmte Funktion aus, bis eine Taste gedrückt wird  
  Timer tmr = new Timer(...);  
  Console.ReadKey();  
  tmr.Dispose();  
}  
  

Ohne Dispose würde der GC beim nächsten Collect auch fälschlicherweise die Tmr-Instanz wegräumen, da der GC erkennt, dass die Variable nicht mehr gebraucht wird. (Ein setzen auf null würde auch nicht helfen!)

Hmmm... hat das mal jemand getestet?

Folgender Code:


using System;
using System.Collections.Generic;
using System.Text;
using System.Timers;

namespace GenTest {
  class Program {
    static int p_ElapsedNr = 0;

    static bool GetKey() {
      ConsoleKeyInfo keyInfo = Console.ReadKey();

      if( keyInfo.Key == ConsoleKey.C ) {
        Console.WriteLine( "Collecting..." );
        GC.Collect();
      }
      else if( keyInfo.Key == ConsoleKey.Z ) {
        return false;
      }

      return true;
    }

    static void Main( string[] args ) {
      Timer tmr = new Timer( 5000 );
      tmr.Elapsed += ElapsedHandler;
      tmr.AutoReset = true;
      tmr.Enabled = true;

      while( GetKey() ) ;
    }

    static void ElapsedHandler( object obj, ElapsedEventArgs e ) {
      Console.WriteLine( "Elapsed #" + p_ElapsedNr++ );
    }
  }
}

Das läuft bei mir munter den ganzen Tag weiter und feuert Elapsed-Events, egal wie oft ich "c" drücke. Hab ich da einen Denkfehler oder ist das "frühzeitige Aufräumen" ein Mythos?

PS: das läuft auch munter eventsfeuernd weiter, wenn ich ein "tmr = null;" vor die while-Schleife setze.

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo Solid96,

Das läuft bei mir munter den ganzen Tag weiter ...

im Debug- oder im Release-Modus?

ist das "frühzeitige Aufräumen" ein Mythos?

Vom Grundsatz kein Mythos. Wir hatten zum einen schon Fragen zu Timern, die plötzlich aufhören zu feuern und zum anderen auch andere zunächst verblüffende Situationen, in denen der CG Objekte freigibt (MemoryLeaks / nicht abgehängte Events).

Andersherum kann u.U. der EventHandler das Zerstören verhindern (MemoryLeaks / nicht abgehängte Events).

herbivore

S
18 Beiträge seit 2009
vor 14 Jahren

Zur ersten Frage: Release-Build.

Um das ganze noch ein wenig auf die Spitze zu treiben:


using System;
using System.Collections.Generic;
using System.Text;
using System.Timers;

namespace GenTest {
  class MyTimer : Timer {

    public int[] Mem = null;
    public string Name = "";

    public MyTimer( int msecs, string name ) : base( msecs ) {
      Name = name;
      Mem = new int[ 1024 * 1024 * 100 ];

      for( int i = 0; i < Mem.Length; ++i ) {
        Mem[ i ] = i;
      }
    }

    ~MyTimer() {
      Console.WriteLine( "Destructed " + Name + "!" );
    }
  }

  class Program {
    static int p_ElapsedNr = 0;

    static bool GetKey() {
      ConsoleKeyInfo keyInfo = Console.ReadKey();

      if( keyInfo.Key == ConsoleKey.C ) {
        Console.WriteLine( "Collecting..." );
        GC.Collect();
      }
      else if( keyInfo.Key == ConsoleKey.Z ) {
        return false;
      }

      return true;
    }

    static void Test1() {
      MyTimer tmr = new MyTimer( 5000, "T1" );
      tmr.Elapsed += ElapsedHandler;
      tmr.AutoReset = true;
      tmr.Enabled = true;

      while( GetKey() ) ;

      tmr.Elapsed -= ElapsedHandler;
      tmr.Stop(); // Holla, erst das Stoppen des Timers gibt den Timer anscheinend zum Aufräumen frei
    }

    static void Test2() {
      MyTimer tmr = new MyTimer( 5000, "T2" );
      tmr.Elapsed += ElapsedHandler;
      tmr.AutoReset = true;
      tmr.Enabled = true;

      while( GetKey() ) ;

      tmr.Elapsed -= ElapsedHandler;
      tmr.Stop(); // Dasselbe hier
    }

    static void Main( string[] args ) {
      Test1();
      Test2();

      while( GetKey() ) ;
    }

    static void ElapsedHandler( object obj, ElapsedEventArgs e ) {
      Console.WriteLine( "Elapsed #" + p_ElapsedNr++ + "  " + (obj as MyTimer).Name );
    }
  }
}

Wenn man die Zeilen "tmr.Stop();" auskommentiert, werden die beiden Timer-Instanzen nie aufgeräumt, ob nun der Event abgehangen wurde oder nicht. Erst, wenn man den Timer stoppt, fasst der GC ihn an.

Ich denke mal, das ist so ziemlich das Gegenteil von "frühzeitigem Aufräumen" und evtl. die Reaktion von MS auf ein früheres Fehlverhalten?

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo Solid96,

evtl. die Reaktion von MS auf ein früheres Fehlverhalten?

möglich. Mit dem Reflector sollte man das herausfinden können.

Außerdem gibt es ja drei verschiedene Timer-Klassen, die sich möglicherweise auch in dieser Hinsicht unterschiedlich verhalten.

herbivore

3.971 Beiträge seit 2006
vor 14 Jahren

Ohne Dispose gehts nicht! Der GC räumt das Timer Objekt weg.
Hier ein kleines Testprogramm, getestet unter Release und .NET Version 3.5


using System.Threading; //Timer

static void Main(string[] args)
{
	Timer tmr = new Timer(Callback, null, 0, 1000);

	int GC_Begin = GC.CollectionCount(0);

	for (int i = 0; i < 100000000; i++)
	{
		object obj = new object();
		obj.ToString();
	}

	int GC_End = GC.CollectionCount(0);

	Console.WriteLine("Fertig");
	Console.WriteLine("Anzahl der GC Collections: {0}", GC_End - GC_Begin);
	Console.ReadKey();
}

static void Callback(object state)
{
	Console.WriteLine("Timer_Callback");
}

In deinem Script wird vermutlich in der Zeit kein GC ausgeführt.

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...

S
18 Beiträge seit 2009
vor 14 Jahren

Hallo,

also mal der Reihe nach...

@herbivore

Der Tip mit den verschiedenen Timern und Reflector war schonmal sehr gut. g

System.Timers.Timer verwendet intern ein System.Threading.Timer Objekt. Der Knackpunkt liegt dann auch bei der System.Threading.Timer Implementation. Dazu später mehr...

@kleines_eichhörnchen

Doch, es geht ohne Dispose.

Ersetze bei deinem Beispiel die Zeile "Timer tmr = new Timer(Callback, null, 0, 1000);" durch diese beiden Zeilen:


Timer tmr = new Timer( Callback );
tmr.Change( 0, 1000 );

Was wirst du festellen? Richtig. Der Timer wird nicht aufgeräumt und läuft schön weiter.

So, und nun zum Knackpunkt: nämlich dem state-objekt bzw. der Referenz darauf.

Wird dem System.Threading.Timer Konstruktur als state-objekt ein Verweis auf sich selbst gegeben (z.B. wie oben indem man den Konstruktor mit nur einem Parameter verwendet), wird er nicht vom GC aufgeräumt, andernfalls wird er ganz normal vom GC aufgeräumt, wie man es erwarten würde.

Tiefer bin ich nicht eingestiegen, aber ich denke es wird irgendwo statisch einen "echten" Timer geben, der dann einfach eine Menge Einträge ala Callback/StateObjekt/dueTime/Intervall enthält. Und diese Einträge beinhalten dann halt auch evtl. Verweise auf die System.Threading.Timer Objekt selbst (state Objekt oder callback). Dieser "echte" Timer wird nie aufgeräumt. D.h. die Einträge werden nur entfernt, wenn die "übergeordneten" Timer-Objekte entweder von Hand disposed werden, oder aber durch den GC (sofern der entsprechende Eintrag im "echten" Timer keinen Verweis auf das abzuräumende Timer-Objekt enthält).

Abschließend nochmal zu kleines_Eichhörnchens Bemerkung in meinem Script würde der GC nicht ausgeführt: doch das wird er, sogar auf Tastendruck. g

Es ist nur so: System.Timers.Timer Objekte werden NIE (eher bekommst du eine OutOfMemory-Exception) vom GC aufgeräumt, wenn sie nicht von Hand gestoppt (System.Timers.Timer.Stop() - was gleichbedeutend mit Enabled = false ist - was gleichbedeutend mit einem Dispose auf das interne System.Threading.Timer Objekt ist) werden. Dies ist wahrscheinlich deshalb so, weil System.Timers.Timer ein System.Threading.Timer Objekt instanziiert und zwar mit einem Verweis auf seine eigene Callback-Funktion (die ja dann erst das Elapsed-Event feuert).

Naja, zumindest weiß ich jetzt, dass das frühzeitige Aufräumen auf keinen Fall ein Mythos ist... aber auch, dass es nicht mit allen Timern geschieht, und vor allem WARUM es geschieht - oder eben manchmal auch nicht. 😉

3.971 Beiträge seit 2006
vor 14 Jahren

Die Frage von dem ganzen ist, wie weit erkennt und optimiert der Compiler (C#-Compiler und JIT-Compiler) nicht mehr verwendete Variablen und Instanzen PUNKT!

Die Timer-Klasse (oder BaseTimer) speichert keinerlei Instanzen in statischen Listen oder ruft GC.KeepAlive auf (Quelle, Timer.cs .NET SourceCode). Das Problem an der Sache ist, a) du weißt nicht wie und was der Compiler optimiert und b) du weißt nicht, ob das in der nächsten Version des C#-Compiler oder des JIT-Compilers noch genauso sein wird.

Es gibt 3 Arten von Menschen, die die bis 3 zählen können und die, die es nicht können...

S
18 Beiträge seit 2009
vor 14 Jahren

@kleines_eichhörnchen

Sorry, aber PUNKT ist in meinen Augen kein Argument - ich gehe den Sachen ganz gerne auf den Grund. Klar ist, dass System.Timers.Timer NIE von selbst aufgeräumt wird (sobald intern das System.Threading.Timer Objekt erstellt wurde), System.Threading.Timer dann nicht, wenn als state Objekt ein Verweis auf sich selbst geliefert wird.

Daraus folgt, dass dieser Verweis (und der auf den Callback) noch in einem anderen Objekt gespeichert wird. Ob das eine statische Liste oder was auch immer ist, kann evtl. jemand mit mehr Zeit eruieren, jedenfalls ist das anscheinend der Grund, warum System.Timers.Timer gar nicht (Verweis auf Callback), und System.Threading.Timer beim Erstellen per Konstruktur mit einem Parameter nicht aufgeräumt wird (Verweis auf sich selbst).

Das hat jetzt weniger was mit Optimierung, als mit der ganz simplen Tatsache zu tun, dass ein Objekt nicht aufgeräumt werden kann, wenn noch Verweise darauf existieren.

Wenn du es natürlich GENAU weißt, dann immer her mit der Info. Ich weiß zumindest nun, dass sich System.Timers.Timer und System.Threading.Timer sehr unterschiedlich verhalten, was das "frühzeitige Aufräumen" angeht. 😉

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo Solid96,

Sorry, aber PUNKT ist in meinen Augen kein Argument

ich glaube was kleines_eichhoernchen sagen wollte ist, dass alle Tests, die man durchführen kann, nur eine begrenzte Aussagekraft haben, weil es keine feststehende Spezifikation des Verhaltens gibt, sondern sich dieses schon in der nächsten Version oder sogar schon im nächsten Servicepack ändern kann. Man muss also sowieso vom worse case ausgehen und gegen diesen robust programmieren.

wenn als state Objekt ein Verweis auf sich selbst geliefert wird.

da der GC in der Lage ist, zyklische Verweise zu erkennen und zu ignorieren, ist das zumindest sehr merkwürdig.

..., dass ein Objekt nicht aufgeräumt werden kann, wenn noch Verweise darauf existieren.

So stimmt das nicht. Der GC durchaus in der Lage, Objekte aufzuräumen, wenn er erkennt, dass eine Referenz zwar noch vorhanden ist, aber nicht weiter benutzt wird.

herbivore

S
18 Beiträge seit 2009
vor 14 Jahren

Hallo herbivore,

ich glaube was kleines_eichhoernchen sagen wollte ist, dass alle Tests, die man durchführen kann, nur eine begrenzte Aussagekraft haben, weil es keine feststehende Spezifikation des Verhaltens gibt, sondern sich dieses schon in der nächsten Version oder sogar schon im nächsten Servicepack ändern kann. Man muss also sowieso vom worse case ausgehen und gegen diesen robust programmieren.

Ok, dann hab ich das wohl falsch verstanden. Hörte sich für mich alles ein wenig nach "es kann nicht sein, was nicht sein darf" an. Ja, man müsste eigentlich in beiden Fällen (System.Timers.Timer und System.Threading.Timer) in dem Beispielszenario erwarten, dass die Timer frühzeitig aufgeräumt werden. Auch klar ist, dass ein Dispose beide Fälle korrekt löst.

da der GC in der Lage ist, zyklische Verweise zu erkennen und zu ignorieren, ist das zumindest sehr merkwürdig.

Es ging ja noch weiter... ich schrieb danach noch:

Daraus folgt, dass dieser Verweis (und der auf den Callback) noch in einem anderen Objekt gespeichert wird.

Und eben das dürfte dann der Grund sein, warum der Timer in dem Fall überhaupt nicht mehr vom GC angefasst wird.

Man kann sich also auch nicht darauf verlassen, dass ein Timer überhaupt aufgeräumt wird. Beide implementieren nicht ohne Grund Dispose, ich denke da sind wir uns dann wieder einig. 😃