Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
Timer und Dispose
Kuehner
myCSharp.de - Member



Dabei seit:
Beiträge: 492

Themenstarter:

Timer und Dispose

beantworten | zitieren | melden

Hallo,

Wenn ich die Dispose()-Funktion des Timers aufrufe, stoppt dann auch der Timer auch oder wird die Timer-Funktion trotzdem noch aufgerufen?
private Nachricht | Beiträge des Benutzers
DiViP
myCSharp.de - Member



Dabei seit:
Beiträge: 27

beantworten | zitieren | melden

Was hindert dich daran es auszuprobieren ? :)
private Nachricht | Beiträge des Benutzers
Kuehner
myCSharp.de - Member



Dabei seit:
Beiträge: 492

Themenstarter:

beantworten | zitieren | melden

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ß...
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

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?"
private Nachricht | Beiträge des Benutzers
kleines_eichhoernchen
myCSharp.de - Member

Avatar #avatar-2079.jpg


Dabei seit:
Beiträge: 4055
Herkunft: Ursprünglich Vogtland, jetzt Much

beantworten | zitieren | melden

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...
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
kleines_eichhoernchen
myCSharp.de - Member

Avatar #avatar-2079.jpg


Dabei seit:
Beiträge: 4055
Herkunft: Ursprünglich Vogtland, jetzt Much

beantworten | zitieren | melden

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...
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
gespannter_Bogen
myCSharp.de - Member



Dabei seit:
Beiträge: 40

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Solid96
myCSharp.de - Member



Dabei seit:
Beiträge: 18

beantworten | zitieren | melden

Zitat von kleines_eichhoernchen
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.
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

Hallo Solid96,
Zitat
Das läuft bei mir munter den ganzen Tag weiter ...
im Debug- oder im Release-Modus?
Zitat
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
private Nachricht | Beiträge des Benutzers
Solid96
myCSharp.de - Member



Dabei seit:
Beiträge: 18

beantworten | zitieren | melden

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?
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

Hallo Solid96,
Zitat
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
private Nachricht | Beiträge des Benutzers
kleines_eichhoernchen
myCSharp.de - Member

Avatar #avatar-2079.jpg


Dabei seit:
Beiträge: 4055
Herkunft: Ursprünglich Vogtland, jetzt Much

beantworten | zitieren | melden

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...
private Nachricht | Beiträge des Benutzers
Solid96
myCSharp.de - Member



Dabei seit:
Beiträge: 18

beantworten | zitieren | melden

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. ;)
private Nachricht | Beiträge des Benutzers
kleines_eichhoernchen
myCSharp.de - Member

Avatar #avatar-2079.jpg


Dabei seit:
Beiträge: 4055
Herkunft: Ursprünglich Vogtland, jetzt Much

beantworten | zitieren | melden

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...
private Nachricht | Beiträge des Benutzers
Solid96
myCSharp.de - Member



Dabei seit:
Beiträge: 18

beantworten | zitieren | melden

@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. ;)
private Nachricht | Beiträge des Benutzers
herbivore
myCSharp.de - Experte

Avatar #avatar-2627.gif


Dabei seit:
Beiträge: 52329
Herkunft: Berlin

beantworten | zitieren | melden

Hallo Solid96,
Zitat
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.
Zitat
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.
Zitat
..., 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
private Nachricht | Beiträge des Benutzers
Solid96
myCSharp.de - Member



Dabei seit:
Beiträge: 18

beantworten | zitieren | melden

Hallo herbivore,
Zitat
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.
Zitat
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:
Zitat
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. :)
private Nachricht | Beiträge des Benutzers