Autor Beitrag
Määx
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 123



BeitragVerfasst: Mo 10.02.14 21:11 
Hallo zusammen,

ich habe diverse Forms bzw UserControls bei denen ich System.Timers.Timer nutze. Diese starte ich it myTimer.Start() spätestens in meinem Destruktor rufe ich die myTimer.Stop() auf.
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
~MyClass()
{
  myTimer.Stop();
  myTimer.Dispose();
}


Setze ich nun einen Breakpoint auf den Konstukrot wird dieser nie aufgerufen. Nun habe ich es manchmal, dass beim schliessen der Form der Prozess nicht richtig beendet wird und vermute, dass es daran liegt, dass der Timer nicht richtig beendet wird.
Was mache ich da falsch? Warum wird der destruktor nicht aufgerufen? Hat da jemand eine Idee?

Vielen Dank
Määx
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4708
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Mo 10.02.14 21:41 
Das ist kein Destruktor es ist ein Finalizer und der wird aufgerufen wenn der Garbage Collector meint das es Zeit ist den aufzurufen unter Umständen, wenn das System meint es ist unnötig aufzuräumen, passiert das nie bzw. erst beim sauberen Beenden des Prozesses.

Am Timer hängen Systemresourcen die sollten explizit zum richtigen (frühestmöglichen) Zeitpunkt freigegeben werden darum implementiert der Timer ja auch IDisposable damit man da explizit diese Resourcen freigeben kann. Vermutlich sollte deine MyClass auch IDisposable implementieren und du in der Dispose Methode von MyClass Dispose vom Timer aufrufen. Als letzte ~Verteidigungslinie~ kannst du dann im Finalizer die eigene Dispose Methode aufrufen falls du im Code selbst vergessen hast Dispose aufzurufen. Für das typische Dispose Pattern siehe die Msdn.

Stop des Timers stoppt das feuern neuer Elapsed Events beendet aber nicht eventuell noch laufende Elapsed Threads. Vielleicht musst du noch das Ende der Threads abwarten weil du irgendwas lang laufendes in dem Event veranstaltest.
Määx Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 123



BeitragVerfasst: Mo 10.02.14 21:54 
Hey,

danke für die schnelle Antwort. Verstehe jetzt zwar das Prinzip, bin mir jedoch nicht ishcer wie ich es umsetzen kann. Das IDisposable hatte ich auch gefunden und probiert. Leider geht es teilweise aber eben um ein UserControl, welches ja selbst IDisposable implementiert (im durch den Designer erstellten Code) und somit kann ich das ja nicht selbst ein weiteres mal überschreiben?

Vielen Dank
Määx

Edit: Habe die Dispose(bool) jetzt einfach aus dem Designer-Code ausgeschnitten und bei mir eingefügt - das klappt zwar, kommt mir aber irgendwie falsch vor?
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4708
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Mo 10.02.14 22:09 
Im Designer Code gibts Dispose ? Da werden maximal Events mit Methoden verknüpft falls ein Control ein Disposed Event hat. Aber die Dispose Implementierung selbst liegt im Framework die kann man nicht einfach hin und herkopieren.

Edit: Ok es wird eine Dispose Methode generiert. Warum auch immer war mir nicht bewußt. Controls haben ein explizites Disposed Event das man verdrahten kann. Den generierten Code solltest du in Ruhe lassen.
Määx Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 123



BeitragVerfasst: Mo 10.02.14 22:35 
oh verdammt - das hatte ich erst gesucht, aber im Designer zeigt mir das VisualStudio das Event nicht an - mit this.Disposed kann ich aber eins hinzufügen :)
Super, Danke! Dann ist es ja recht "einfach" :)
Määx Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 123



BeitragVerfasst: Do 20.02.14 11:47 
ok, irgendwie bekomme ich das nicht hin :(
Ich habe insgesamt folgendes Design: Aus meiner MainApp starte ich mehrere Fenster, denen ich ein "globales" Objekt Manager mitgebe, auf dessen events sie reagieren können. Eines der Fenster startet dann eben einen Timer. Aussehen sieht das stark reduziert dann wie folgt:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
public partial class MainApp : Form
{
  private Manager _manager = null;
  private WindowA _windowA = null;

  public MainApp()
  {
    manager = new Manager();
    OpenWindowA();
  }
  
  public void OpenWindowA()
  {
    if(_windowA==null)
    {
      _windowA = new WindowA(_manager);
      _windowA.Disposed += _windowA_disposed;
    }
  }
  private void _windowA_disposed(object sender, EventArgs e)
  {
    _windowA = null;
  }
}


Mein WindowA besteht aus einer Form, einem Form-Interface und einem Presenter:
ausblenden volle Höhe C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
public partial class WindowA: Form, IWindowA
{
  private WindowAPresenter _presenter = null;
  private System.Timers.Timer _timer;

  public WindowA(Manager manager)
  {
    _presenter = new WindowAPresenter(this, manager);
    this.Disposed += WindowA_Disposed;  //hatte ich auch schonmal durch die FormClosing ersetzt
    _timer = new System.Timers.Timer(1000);
    _timer.Elapsed += timer_Elapsed;
  }
  void WindowA_Disposed(object sender, EventArgs e)
  {
    _presenter.Stop();
    _timer.Elapsed -= timer_Elapsed;
    _timer.Stop();
    _timer.Dispose();
  }
  private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
    if (textBox.InvokeRequired)
      textBox.Invoke(new Action(() => { textBox.Text = "Test"; }));
    else
      textBox.Text = "Test";
  }
  internal void SetText(string value)
  {
    if (textBoxPresenter.InvokeRequired)
      textBoxPresenter.Invoke(new Action(() => { textBoxPresenter.Text = value; }));
    else
      textBoxPresenter.Text = value;
  }
}

ausblenden volle Höhe C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
public partial class WindowAPresenter
{
  private Manager _manager = null;
  private IWindowA _view = null;
  private System.Timers.Timer _timer;

  public WindowAPresenter(IWindowA view, Manager manager)
  {
    _manager = manager;
    _view = view;
    _timer = new System.Timers.Timer(1000);
    _timer.Elapsed += timer_Elapsed;
    
    _manager.Started += _manager_Started;
    _manager.Stoped += _manager_Stoped;
  }
  ~WindowAPresenter()
  {
    _timer.Dispose();
  }
  internal void Stop()
  {
    _manager.Started -= _manager_Started;
    _manager.Stoped -= _manager_Stoped;
    _timer.Elapsed -= timer_Elapsed;
    _timer.Stop();
  }
  private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
    _view.SetText("test");
  }
  private void _manager_Started(object sender, EventArgs e)
  {
    _Timer.Start();
  }
  private void _manager_Stoped(object sender, EventArgs e)
  {
    _Timer.Stop();
  }
}


Jetzt bekomme ich -manchmal- folgende Fehlermeldungen. Zum einen in der WindowA.SetText - Methode die aussage, dass auf das verworfene TextFeld nicht mehr zugegriffen werden kann (Hier scheint also noch der Timer weiter zulaufe, obwohl ich alles beim FormClosing/Disposed stoppe. Und zum anderen erhalte ich im WindowsA-Designer-Code in der überschirebenen Dispose-Methode an der stelle base.Dispose folgende Fehlermeldung:
ausblenden Quelltext
1:
Es wurde versucht, einen RCW freizugeben, der derzeit benutzt wird. Der RCW wird im aktiven Thread oder einem anderen Thread verwendet. Der Versuch, einen RCW freizugeben, der gerade verwendet wird, kann Datenbeschädigung oder -verlust zur Folge haben.					


Ich hatte auch versucht in die TimerElapsed-Funktionen jeweil am Anfang eine bool-Variable timerIsActive auf true zu setzen und am Ende auf false und in der WindowA.dispose/formClosing bzw WindowAPresenter.Stop jeweil eine while(timerIsActive); vor dem timer.Dispose einzuubauen. Dabei habe ich dann aber endlosschleifen generiert...

Ich habe jetzt echt keine Idee mehr, wie ich die Timer nun korrekt nutzen muss? Kann mir jemand erklären wo mein Fehler liegt?
Vielen vielen Dank
Määx

Edit: gut das mit Endlosschleife ist ja logisch, da ich im TimerElapsed ja den GUI-Thread joinen muss. War ne dumme Idee :)
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4796
Erhaltene Danke: 1059

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: Do 20.02.14 12:35 
Hallo Määx,

warum benutzt du nicht den System.Windows.Forms.Timer? Dann bräuchtest du nicht den Zugriff auf UI-Elemente mittels Control.Invoke durchführen und hättest auch wahrscheinlich mit dem Stoppen und Disposen weniger Probleme.
Der System.Timers.Timer ist ja hauptsächlich für Dienste oder aber Hintergrundprozesse gedacht.
Määx Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 123



BeitragVerfasst: Do 20.02.14 14:28 
mh, dachte immer, man soll den nicht verwenden weil er den GUI-Thread verlangsamt. Aber ich probier es mal aus. Muss ich dann auch dispose usw. aufrufen oder soll ich das dann ignorieren?

Edit: Wunderbar! Habe den Timer ersetzt und keine Probleme mehr! Klasse! Aber aus interesse würde ich trotzdem ganz gerne verstehen wie ich es richtig lösen müsste?
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4796
Erhaltene Danke: 1059

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: Do 20.02.14 14:39 
Hallo,

natürlich kommt es drauf an, was du in der Timer-Routine machen willst, d.h. wie lange ein Tick dauert (der UI-Thread sollte nicht mehr als 10-50ms unterbrochen werden).
Bisher delegierst du ja den gesamten Timer.Tick (bzw. Elapsed) zurück in den UI-Thread, d.h. du gewinnst nichts dadurch.

Als Alternative würde ich dann aber eher den System.Threading.Timer verwenden.

Ich selber hatte bisher noch keine Probleme, den Timer zu stoppen (und den Rest vom GC erledigen zu lassen).
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4708
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Do 20.02.14 14:58 
Zitat:
Als Alternative würde ich dann aber eher den System.Threading.Timer verwenden.


Der zuerst benutzte Timer kapselt den Threading.Timer (ich glaube hauptsächlich damit man einen Event hat und nicht diesen doofen Callback Syntax). Das verhalten sollte also eigentlich identisch sein und dann genauso knallen wenn man einen synchronisierten Zugriff auf die UI macht ohne zu prüfen ob die Form disposed ist.

Wenn das einzige was Elapsed tut updaten der Form ist dann kann man auch gleich den ganzen Event im Hauptthread ausführen lassen. Dazu einfach die SynchronizingObject Property des Timers (System.Timers.Timer um genau zu sein) auf die Form setzen. Darin zur Sicherheit prüfen ob den die Form disposed ist.

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
// im WindowA Constructor 
_timer.SynchronizingObject = this;

private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
   if(!textBox.IsDisposed) // oder this anstatt textBox
      textBox.Text = "Test";
}


Oder eben einfach den auf WM_Timer beruhenden Winforms Timer nehmen. Ohne Threads kein Synchronisierungsproblem.