Laden...

[erledigt] Viele Methoden verzögert ausführen: Timer wird vorzeitig vom GC freigegeben?

Erstellt von Master15 vor 12 Jahren Letzter Beitrag vor 12 Jahren 6.484 Views
M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 12 Jahren
[erledigt] Viele Methoden verzögert ausführen: Timer wird vorzeitig vom GC freigegeben?

Hallo C#ler,

ich habe eine Methode, die von diversen anderen Threads aufgerufen werden kann. Diese soll etwas anschalten. Ist eine Impulsdauer angegeben, soll es automatisch wieder ausgeschaltet werden. Der Aufrufer darf aber nicht blockiert werden (z.B. durch Thread.Sleep(impuls)).

Das Problem habe ich mal in eine separate Testanwendung gepackt:


/// <summary>
/// Aktiviert irgendwas
/// </summary>
/// <param name="impuls">Impuls in ms für automatische Abschaltung</param>
public void Aktiviere(int impuls)
{
    SchalteAn();

    if (impuls > 0)
        new System.Threading.Timer(t => { SchalteAus(); }, null, impuls, Timeout.Infinite);
}

private void SchalteAn()
{
    Debug.WriteLine(DateTime.Now.ToString("mm:ss.fff") + " Schalte an");
}

private void SchalteAus()
{
    Debug.WriteLine(DateTime.Now.ToString("mm:ss.fff") + " Schalte aus");
}

Ich habe jetzt mehrfach in der Testanwendung Aktiviere(200) aufgerufen und festgestellt, dass nicht immer die Ausgabe "Schalte aus" erscheint.
Meine Vermutung ist, dass mir da der Garbage Collector einen Strich durch die Rechnung macht und meinen Timer freigibt, da kein Verweis darauf vorhanden ist. Laut MSDN scheint meine Vermutung zu stimmen.

Ersetzt man den Timer durch einen Task, funktioniert es scheinbar immer:


Task.Factory.StartNew(() =>
{
    Thread.Sleep(impuls);
    SchalteAus();
});

Macht das mit dem Task Sinn, oder gibt es beim Timer irgendeinen Trick, dass das immer funktioniert?
Gibt es eventuell eine andere/bessere Lösung, eine Methode verzögert auszuführen?

Gruß
Thomas

Edit: [erledigt]

5.742 Beiträge seit 2007
vor 12 Jahren

Hallo Master15,

du musst den Timer in einer Instanzvariable speichern, da er sonst vom GC entsorgt wird.
Zudem musst du ihn evtl. manuell starten.

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 12 Jahren

Hallo winSharp93,

du musst den Timer in einer Instanzvariable speichern, da er sonst vom GC entsorgt wird.

Die Instanzvariable würde dann aber bei jedem Aufruf von Aktiviere(200) wieder überschrieben werden. D.h. nur auf den letzten erzeugten Timer hat man dann durch die Instanzvariable einen Verweis.
Wenn ich das richtig verstehe, wäre das sogar noch schlimmer, da so ein Fehler noch schwieriger zu finden ist.

Vielleicht gibt es ja noch andere Ideen.

Gruß
Thomas

16.834 Beiträge seit 2008
vor 12 Jahren

Dann hast Du eben eine Liste von Timern als Instanzvariable...?

5.742 Beiträge seit 2007
vor 12 Jahren

Dann hast Du eben eine Liste von Timern als Instanzvariable...?

Und damit diese dann nicht zu groß wirst, solltest du jeden Timer, sobald er feuert, disposen und aus der Liste rauswerfen.

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 12 Jahren

hmmm... irgendwie hatte ich es geahnt, dass das mit der Liste kommt. Nagut, werde ich mir mal überlegen. Eventuell lasse ich auch den Task.
Trotzdem Danke! 😉

5.742 Beiträge seit 2007
vor 12 Jahren

eventuell lasse ich auch den Task.

Ist aber eine ganz schlechte Idee, da es ziemlich viele Resourcen zieht, lauter sleepende Threads in der eigenen Anwendung zu haben.
Evtl. führt das letztlich sogar dazu, dass dein kompletter TaskScheduler blockiert und letztlich die Wartezeiten überhaupt nicht mehr stimmen.

Aber gut - eine Liste wäre wohl zu einfach.

U
1.688 Beiträge seit 2007
vor 12 Jahren

Eigentlich brauchst Du ja nur einen Timer, der regelmäßig aufgerufen wird. In der Liste stecken Aktionen mit Zeiten. Du musst dann eben alle Aktionen ausführen, deren Zeit "vergangen" ist.

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 12 Jahren

Guten Abend,

Ist aber eine ganz schlechte Idee, da es ziemlich viele Resourcen zieht, lauter sleepende Threads in der eigenen Anwendung zu haben.
Evtl. führt das letztlich sogar dazu, dass dein kompletter TaskScheduler blockiert und letztlich die Wartezeiten überhaupt nicht mehr stimmen.

Ich muss gestehen, dass ich mir darüber noch keine Gedanken gemacht habe. Gut dieses Problem hatte ich auch noch nie. Normalerweise nutze ich einen Timer, um Dinge zyklisch auszuführen und da hat man dann natürlich einen Verweis drauf.

Theoretisch angenommen man hat 1000 Tasks, die mittels Thread.Sleep, WaitHandle (ManualResetEvent, AutoResetEvent) blockiert werden. Heißt das jetzt, dass auch tatsächlich 1000 Threads blockieren?

Aber gut - eine Liste wäre wohl zu einfach.

Um das gehts mir eigentlich gar nicht. Ich versuche nur von vornherein eine Software so zu programmieren, dass die verwendeten Listen auch einen Sinn für die eigentliche Anwendung ergeben. In diesem Fall pfeift aber irgendwie mein innerer Schweinehund, eventuell mal wieder unbegründet. 😁
Aber wenn das mit einem Task wirklich nicht vertretbar ist und es keine andere Möglichkeit gibt, dass man eine Methode verzögert ausführen kann, dann werde ich das so machen müssen.

Gruß
Thomas

49.485 Beiträge seit 2005
vor 12 Jahren

Hallo Master15,

Theoretisch angenommen man hat 1000 Tasks, die mittels Thread.Sleep, WaitHandle (ManualResetEvent, AutoResetEvent) blockiert werden. Heißt das jetzt, dass auch tatsächlich 1000 Threads blockieren?

die Task-Klasse verwendet ThreadPool-Threads. Deren Anzahl ist begrenzt. Deshalb wird es wohl keine gleichzeitigen 1000 Threads geben. Insofern warten später erstellte Tasks bis wieder Threads frei werden. Sind die vorhandene Threads z.B. alle mit langen Wartezeiten blockiert (und natürlich blockiert Thread.Sleep den Thread), dann werden neue Tasks mit kurzen Wartezeiten trotzdem erst ausgeführt, wenn genug der lange blockierten Threads wieder frei sind.

Nicht getestet, aber so müsste das der Theorie nach sein.

Ich versuche nur von vornherein eine Software so zu programmieren, dass die verwendeten Listen auch einen Sinn für die eigentliche Anwendung ergeben.

Darüber, ob eine Liste von Timern für die eigentliche Anwendung Sinn macht, kann man in der Tat geteilter Meinung sein. Wenn man mit vielen Timern arbeiten will, ist sie zumindest technisch nötig.

Eine Liste der (noch) anstehenden Aktionen, wie sie ujr vorgeschlagen hat, macht wohl auf jeden Fall Sinn. Jedenfalls wäre das auch meine Empfehlung. Ich hatte selbst überlegt, das vorzuschlagen, noch bevor ich ujrs Vorschlag gesehen habe. Vor allem, weil man dann nur einen Timer braucht, den man jeweils auf die Differenz von aktueller Zeit und der Zielzeit der nächsten Aktion stellt. Bei Timer Event führt man dann alle Aktionen aus, deren Zielzeit schon verstrichen ist und wegen der ohnehin vorhandenen Ungenauigkeiten auch gleich die, deren Zielzeit in den nächsten Millisekunden verstreichen wird.

herbivore

5.742 Beiträge seit 2007
vor 12 Jahren

dann werden neue Tasks mit kurzen Wartezeiten trotzdem erst ausgeführt, wenn genug der lange blockierten Threads wieder frei sind.

Und schlimmer noch: Die Wartezeiten summieren sich dann. D.h. wenn alle Threads blockiert sind und 5 Sekunden nachdem eine neue Aktion einen weiteren Thread starten will, einer frei wird, hat diese Aktion ja bereits 5 Sekunden gewartet und wartet dann nochmal zusätzlich so lange, wie sie eigentlich warten sollte (zumindest in deinem Code).

den man jeweils auf die Differenz von aktueller Zeit und der Zielzeit der nächsten Aktion stellt. Bei Timer Event führt man dann alle Aktionen aus, deren Zielzeit schon verstrichen ist

Problem ist nur, dass man sich nicht darauf verlassen kann, dass die Systemzeit stimmt.
Wenn diese (aufgrund von Zeitsynchronisation, Änderung durch den Benutzer, Beginn der Sommerzeit etc.) plötzlich einen Sprung nach vorne oder hinten macht, hat man ein Problem.

Im Prinzip müsste man quasi für jeden Task eine Stopwatch mitlaufen lassen und dann jeweils ElapsedMiliseconds auswerten oder alternativ Environment.TickCount verwenden (da aber wieder aufpassen, dass es nach einiger Zeit einen Overflow gibt...).

Insofern ist die Variante mit den Timern wohl letztlich deutlich einfacher zu implementieren.

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 12 Jahren

Hallo,

danke erstmal an herbivore für die Erklärung der Task, klingt plausibel.

Ich habe eigentlich vermutet, dass die TPL so intelligent ist, dass einem Task, der längere Zeit durch Thread.Sleep, WaitHandle (ManualResetEvent, AutoResetEvent) blockiert werden, der Thread entzogen wird. Der Thread könnte dann für andere Aufgaben/Tasks genutzt werden.
Leider steht in meinen älteren C#-Büchern nichts drin. Im Internet findet man zwar viel über die Programmierung, aber was im Hintergrund passiert, bleibt offen. Im Openbook Visual C# 2010 Kapitel 11.3 ist es auch nur allgemein formuliert.

Die Lösung von winSharp93 verstehe ich ungefähr so:


private List<Timer> timerList = new List<Timer>();
public void Aktiviere(int impuls)
{
    SchalteAn();

    if (impuls > 0)
    {
        Timer timer = new System.Threading.Timer(t => 
        { 
            SchalteAus();

            //lock (((ICollection)timerList).SyncRoot)
            //    timerList.Remove(timer);

            //timer.Dispose();
        }, null, Timeout.Infinite, Timeout.Infinite);
                
        lock(((ICollection)timerList).SyncRoot)
            timerList.Add(timer);
                
        timer.Change(impuls, Timeout.Infinite); //Manueller Timer-Start
    }
}

Wobei das mit dem Löschen und Dispose noch so eine Sache ist (auskommentiert). Bin gerade überfragt, wie ich einen Verweis auf den Timer am einfachsten übergeben kann.

Zur Lösung mit nur einem Timer von ujr und herbivore:
Verstehe ich euch da richtig? Einen Timer laufen lassen, der z.B. jede 10ms eine Methode aufruft. Die überprüft dann, ob es in einer Liste Einträge gibt (z.B. Aus-Befehl mit TimeStamp), deren Zeit abgelaufen ist. Ist das der Fall wird ein entsprechender Aktion ausgeführt und der Aus-Befehl aus der Liste gelöscht.
Nur in dem Fall frage ich mich was der optimale Wert für das Timer-Intervall ist.

Einfache Dinge können so kompliziert sein... uff...
Mein innerer Timer sagt jetzt erstmal Essenszeit 🙂

Gruß
Thomas

5.742 Beiträge seit 2007
vor 12 Jahren

Ich habe eigentlich vermutet, dass die TPL so intelligent ist, dass einem Task, der längere Zeit durch Thread.Sleep, WaitHandle (ManualResetEvent, AutoResetEvent) blockiert werden, der Thread entzogen wird.

Man kann aber keine Threads "entziehen".
Etwas in die Richtung ist nur mit .ContinueWith bzw. .ContinueWhenAll möglich (in diesem Kontext aber nicht sinnvoll einsetzbar). Aber auch da verzichtet der Code quasi frewillig auf den Thread.

Die Lösung von winSharp93 verstehe ich ungefähr so:

Im großen und ganzen, ja.

Wobei das mit dem Löschen und Dispose noch so eine Sache ist (auskommentiert). Bin gerade überfragt, wie ich einen Verweis auf den Timer am einfachsten übergeben kann.


Timer timer = null;
timer = new //...

und schon kannst du in der anonymen Methode auf den Timer zugreifen.

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 12 Jahren
  
Timer timer = null;  
timer = new //...  
  

und schon kannst du in der anonymen Methode auf den Timer zugreifen.

Ok, super! Stimmt, darum hat es bei meinem Test vorhin nicht funktioniert, da ich es nicht auf null gesetzt habe.

Funktioniert auch wunderbar, "Schalte aus" ist bei impuls > 0 jetzt immer brav da.
Vielen Dank! Dieses Thema ist auf meiner TODO-Liste somit erstmal erledigt 👍

Man kann aber keine Threads "entziehen".
Etwas in die Richtung ist nur mit .ContinueWith bzw. .ContinueWhenAll möglich (in diesem Kontext aber nicht sinnvoll einsetzbar). Aber auch da verzichtet der Code quasi frewillig auf den Thread.

Interessant. Ich nutze statt eines Background-Threads, BackgroundWorker und co neuerdings gerne mal einen Task, u.a. auch wegen .ContinueWith, CancellationTokenSource und der einfacheren Handhabung. Bei .ContinueWith ist mir nur mal aufgefallen, dass dies oftmals auch ein anderer Thread aufruft (Ausgabe von Thread.CurrentThread.ManagedThreadId) und manchmal auch der Gleiche.

U
1.688 Beiträge seit 2007
vor 12 Jahren

Problem ist nur, dass man sich nicht darauf verlassen kann, dass die Systemzeit stimmt.
Wenn diese (aufgrund von Zeitsynchronisation, Änderung durch den Benutzer, Beginn der Sommerzeit etc.) plötzlich einen Sprung nach vorne oder hinten macht, hat man ein Problem.

Zum einen kann man auf Zeitänderungen reagieren (WM_TIMECHANGE abfangen) und den nächsten Zeitpunkt neu berechnen. Zum anderen kann man auch (und das erhöht ggf. die Genauigkeit) den Timer, z. B., jede Sekunde auslösen lassen, und die (sortierte, darum schnell zu analysierende) Aktionsliste überprüfen.

Verstehe ich euch da richtig? Einen Timer laufen lassen, der z.B. jede 10ms eine Methode aufruft. Die überprüft dann, ob es in einer Liste Einträge gibt (z.B. Aus-Befehl mit TimeStamp), deren Zeit abgelaufen ist. Ist das der Fall wird ein entsprechender Aktion ausgeführt und der Aus-Befehl aus der Liste gelöscht.
Nur in dem Fall frage ich mich was der optimale Wert für das Timer-Intervall ist.

Der optimale Wert hängt von der Granularität Deiner Aktionszeitpunkte und von der gewünschten Genauigkeit ab. Außerdem muss man die Genauigkeit der Timer selbst (~55ms, außer beim Multimediatimer) berücksichtigen. Und die Dauer der Aktionen ist auch nicht ganz unwichtig, weil z. B. System.Threading.Timer auch auslöst, wenn der letzte noch nicht fertig ist.

49.485 Beiträge seit 2005
vor 12 Jahren

Hallo winSharp93,

Problem ist nur, dass man sich nicht darauf verlassen kann, dass die Systemzeit stimmt.
Wenn diese (aufgrund von Zeitsynchronisation, Änderung durch den Benutzer, Beginn der Sommerzeit etc.) plötzlich einen Sprung nach vorne oder hinten macht, hat man ein Problem.

ich habe nicht gesagt, dass man sich die Zeit als DateTime merken soll. 😃 Man muss das natürlich in geeigneter Weise tun. Vermutlich bekommt man es mit Environment.TickCount hin.

Hallo Master15,

Verstehe ich euch da richtig? Einen Timer laufen lassen, der z.B. jede 10ms eine Methode aufruft.

nee, ich habe doch beschrieben, wie ich das meine: Den Timer jedes Mal auf das Intervall - also die Zeitdauer - stellen, die bis zur nächsten anstehenden Aktion verbleibt. Man ändert das Intervall also immer passend. Der Timer feuert so nur zu den Zeitpunkten, an denen mindestens eine Aktion ansteht.

herbivore

M
Master15 Themenstarter:in
78 Beiträge seit 2007
vor 12 Jahren

Guddn Tach,

ich habe nicht gesagt, dass man sich die Zeit als DateTime merken soll. 🙂 Man muss das natürlich in geeigneter Weise tun. Vermutlich bekommt man es mit Environment.TickCount hin.

Finde ich für meinen Fall zu kompliziert und da stellen sich mir gleich wieder neue Fragen. Ich bleibe lieber in dieser Anwendung beim KISS-Prinzip.

Der optimale Wert hängt von der Granularität Deiner Aktionszeitpunkte und von der gewünschten Genauigkeit ab.

Genau auch aus diesem Grund bleibe ich erstmal bei der Lösung von winSharp93. So genau muss das nicht sein, nur ungefähr. Wichtig ist nur, dass der SchalteAus-Befehl gesendet wird, sonst brennt u. U. eine Spule durch. Ein paar ms hin oder her sind egal und Realtime wird das mit C# und Managed Code eh nie werden.

[Ironie]Wenn ich mal viiiiiiiiiiiieeeeeeeeel Zeit habe, setze ich ein Gentoo mit Linux Kernel 3 und RT- bzw. RTAI-Patch auf. Dann übergebe ich die Aufgabe per XML/JSON übers Netzwerk an ein Embedded System und dort wird das An- und Abschalten genaustens mittels Realtime Clock ausgelöst.[/Ironie] 😉

Außerdem muss man die Genauigkeit der Timer selbst (~55ms, außer beim Multimediatimer) berücksichtigen. Und die Dauer der Aktionen ist auch nicht ganz unwichtig, weil z. B. System.Threading.Timer auch auslöst, wenn der letzte noch nicht fertig ist.

Hört sich interessant an, werde ich mir mal mit dem Multimediatimer merken. Eventuell braucht man das mal bei einer anderen Anwendung.

Gruß
Thomas