2009-11-09 11 views
16

Ich habe eine System.Threading.Timer, die ihre entsprechenden Event-Handler (Callback) alle 10 ms aufruft. Die Methode selbst ist nicht reentrant und kann manchmal Weg länger als 10 ms nehmen. Daher möchte ich den Timer während der Ausführung der Methode stoppen.Stoppuhr in seiner Callback-Methode

Code:

private Timer _creatorTimer; 

// BackgroundWorker's work 
private void CreatorWork(object sender, DoWorkEventArgs e) { 
     _creatorTimer = new Timer(CreatorLoop, null, 0, 10); 

     // some other code that worker is doing while the timer is active 
     // ... 
     // ... 
} 

private void CreatorLoop(object state) { 
     // Stop timer (prevent reentering) 
     _creatorTimer.Change(Timeout.Infinite, 0); 

     /* 
      ... Work here 
     */ 

     // Reenable timer 
     _creatorTimer.Change(10, 0); 
} 

MSDN besagt, dass die Callback-Methode (jedes Mal, wenn die Timer Feuer) in separaten Thread aus dem Thread-Pool genannt wird. Das bedeutet, dass, wenn ich den Timer stoppe, die erste Sache in der Methode es noch nicht notwendig verhindert, dass der Timer feuert und eine andere Instanz der Methode ausführt, bevor die erste eine Chance hatte, den Timer zu stoppen.

Sollte vielleicht der Timer (oder auch die nicht-reentrante Methode selbst) gesperrt sein? Was ist der richtige Weg, um zu verhindern, dass der Timer während der Ausführung seiner Callback-Methode (und nicht-reentrant) ausgelöst wird?

+0

diese Frage können Sie http://stackoverflow.com/questions/1116249/manualresetevent-vs-thread-sleep – Kane

Antwort

45

Sie könnten den Timer weiterhin die Callback-Methode auslösen lassen, aber Ihren nicht-reentranten Code in einen Monitor.TryEnter/Exit einbinden. In diesem Fall muss der Timer nicht gestoppt/neu gestartet werden. Überlappende Anrufe erhalten die Sperre nicht und kehren sofort zurück.

private void CreatorLoop(object state) 
{ 
    if (Monitor.TryEnter(lockObject)) 
    { 
    try 
    { 
     // Work here 
    } 
    finally 
    { 
     Monitor.Exit(lockObject); 
    } 
    } 
} 
+0

+1 verwenden Ich dachte nie wirklich an eine Verwendung für TryEnter; das ist sehr interessant. – Schmuli

+0

Dies scheint den Trick zu tun. Zwei oder mehr Threads können die Methode eingeben, aber nur eine wird tatsächlich funktionieren. Ich habe auch den Timer nach Monitor.TryEnter() gestoppt, damit er während der Ausführung überhaupt nicht ausgelöst wird (es muss nicht ausgelöst werden), nur für den Fall, dass die Ausführungszeit viel größer ist als die Zeit des Timers. Timer wird neu gestartet, nachdem die Arbeit in Methode abgeschlossen ist. –

+0

+1 Schöne Lösung! – ParmesanCodice

0

Ich hatte eine ähnliche Situation mit einem System.Timers.Timer, wo das verstrichene Ereignis von einem Threadpool ausgeführt wird und wiedereintrittsfähig sein muss.

ich diese Methode, um das Problem zu bekommen verwendet:

private void tmr_Elapsed(object sender, EventArgs e) 
{ 
    tmr.Enabled = false; 
    // Do Stuff 
    tmr.Enabled = true; 
} 

Je nachdem, was Sie tun, Sie System.Timers.Timer betrachten möchten, hier ist eine schöne Zusammenfassung von MSDN

          System.Windows.Forms System.Timers   System.Threading 
Timer event runs on what thread?   UI thread    UI or worker thread Worker thread 
Instances are thread safe?    No      Yes     No 
Familiar/intuitive object model?   Yes      Yes     No 
Requires Windows Forms?     Yes      No     No 
Metronome-quality beat?     No      Yes*     Yes* 
Timer event supports state object?  No      No     Yes 
Initial timer event can be scheduled? No      No     Yes 
Class supports inheritance?    Yes      Yes     No 

* Depending on the availability of system resources (for example, worker threads)    
+0

aushelfen ich glaube, dass diese umgeht das Problem nicht. Nun, es kann in 99,9% -Fällen vorkommen, aber wenn das System Ihrem Event-Handler keine Prozessorzeit vor dem Ereignis Elapsed des nächsten Timers gibt, können zwei verschiedene Threads die Methode parallel ausführen. –

+0

Guter Punkt! Sie könnten dies immer in Verbindung mit einer Sperrlösung wie jsw – ParmesanCodice

6

Ein paar mögliche Lösungen:

  • haben die eigentliche Arbeit in noch einem anderen Thread Delegierten durchgeführt, die auf ein Ereignis wartet. Der Timer-Callback signalisiert lediglich das Ereignis. Der Worker-Thread kann nicht erneut eingegeben werden, da es sich um einen einzelnen Thread handelt, der seine Arbeit nur ausführt, wenn das Ereignis signalisiert wird. Der Timer ist reentrant, da es nur das Ereignis signalisiert (scheint ein kleiner Kreisverkehr und verschwenderisch, aber es wird funktionieren)
  • haben den Timer mit nur einem Start-Timeout und keine periodische Zeitüberschreitung erstellt, so dass es nur einmal ausgelöst wird . Der Timer-Rückruf wird dieses Timer-Objekt entfernen und ein neues erstellen, wenn es seine Arbeit abgeschlossen hat, die auch nur einmal ausgelöst wird.

Sie können in der Lage sein, Option # 2 ohne Entsorgung/Schaffung eines neuen Objekts zu verwalten, indem die Change() Methode des ursprünglichen Timer-Objekt, aber ich bin mir nicht sicher, was das Verhalten genau der ist Change() mit einem neuen Aufruf Startzeitlimit nach Ablauf der ersten Zeitüberschreitung Das wäre ein oder zwei Tests wert.

Edit:


ich den Test tat - die Manipulation der Zeit als eine wiederanlauf one-shot perfekt zu funktionieren scheint, und es ist viel einfacher als die anderen Methoden.Hier ist ein Beispielcode basiert auf Ihnen als Ausgangspunkt (einige Details können geändert haben, um es auf meinem Rechner zu kompilieren):

private Timer _creatorTimer; 

// BackgroundWorker's work 
private void CreatorWork(object sender, EventArgs e) { 
    // note: there's only a start timeout, and no repeat timeout 
    // so this will fire only once 
    _creatorTimer = new Timer(CreatorLoop, null, 1000, Timeout.Infinite); 

    // some other code that worker is doing while the timer is active 
    // ... 
    // ... 
} 

private void CreatorLoop(object state) { 
    Console.WriteLine("In CreatorLoop..."); 
    /* 
     ... Work here 
    */ 
    Thread.Sleep(3000); 

    // Reenable timer 
    Console.WriteLine("Exiting..."); 

    // now we reset the timer's start time, so it'll fire again 
    // there's no chance of reentrancy, except for actually 
    // exiting the method (and there's no danger even if that 
    // happens because it's safe at this point). 
    _creatorTimer.Change(1000, Timeout.Infinite); 
} 
+0

Dies scheint zu funktionieren, ja, aber der Code wird weniger sauber, wenn es viele Ergebnisse der Methode gibt (viele If-Else-Zweige und Ausnahmen). Aber es scheint eine gute Lösung zu sein, wenn es um die Leistung geht, da keine Synchronisationsmechanismen verwendet werden. Ansonsten funktioniert dies NICHT, wenn es andere Methoden/Threads/Timer gibt, die versuchen können, diese Methode einzugeben. Dann treten wir natürlich wieder in die nicht-reentrante Methode ein, bei der Monitore besser funktionieren. Danke für die Lösung und den Test. Es ist eine nette Idee. –

+0

Die Komplexität des Codes ist kein Problem mehr als wenn Sie einen Mutex verwenden - wickeln Sie einfach den Code in ein "try"/"finally" oder rufen Sie einfach eine andere Routine mit der Komplexität auf und lassen Sie die Timer-Callback-Routine ein Einfach 'callen und dann den Timer zurücksetzen'. Wenn der Callback von mehreren Timern verwendet wird, dann funktioniert diese Technik nicht und Sie benötigen ein echtes Synchronisationsobjekt. Ich finde, dass es ziemlich ungewöhnlich ist, dass ein Timer-Callback von mehreren Timern verwendet wird - insbesondere wenn der Callback sehr komplex ist (was im Allgemeinen bedeutet, dass der Timer-Callback für einen bestimmten Zweck ausgelegt ist). –

+0

Sie können das gleiche Verhalten mit System.Timers.Timer erreichen, die ich denke, ist viel einfacher. Siehe http://stackoverflow.com/questions/7055820/non-reentrant-timers/ –

0

Ich mache es mit Verzahnt, die atomare Operationen zur Verfügung stellt und durch CompareExchange stellt sicher, dass nur ein Thread zu einem Zeitpunkt tritt in den kritischen Abschnitt:

private int syncPoint = 0; 

private void Loop() 
    { 
     int sync = Interlocked.CompareExchange(ref syncPoint, 1, 0); 
     //ensures that only one timer set the syncPoint to 1 from 0 
     if (sync == 0) 
     { 
      try 
      { 
       ... 
      } 
      catch (Exception pE) 
      { 
       ... 
      } 
      syncPoint = 0; 
     } 

    } 
0
//using Timer with callback on System.Threading namespace 
    // Timer(TimerCallback callback, object state, int dueTime, int period); 
    //  TimerCallback: delegate to callback on timer lapse 
    //  state: an object containig information for the callback 
    //  dueTime: time delay before callback is invoked; in milliseconds; 0 immediate 
    //  period: interval between invocation of callback; System.Threading.Timeout.Infinity to disable 
    // EXCEPTIONS: 
    //  ArgumentOutOfRangeException: negative duration or period 
    //  ArgumentNullException: callback parameter is null 

    public class Program 
    { 
     public void Main() 
     { 
      var te = new TimerExample(1000, 2000, 2); 
     } 
    } 

    public class TimerExample 
    { 
     public TimerExample(int delayTime, int intervalTime, int treshold) 
     { 
      this.DelayTime = delayTime; 
      this.IntervalTime = intervalTime; 
      this.Treshold = treshold; 
      this.Timer = new Timer(this.TimerCallbackWorker, new StateInfo(), delayTime, intervalTime); 
     } 

     public int DelayTime 
     { 
      get; 
      set; 
     } 

     public int IntervalTime 
     { 
      get; 
      set; 
     } 

     public Timer Timer 
     { 
      get; 
      set; 
     } 

     public StateInfo SI 
     { 
      get; 
      set; 
     } 

     public int Treshold 
     { 
      get; 
      private set; 
     } 

     public void TimerCallbackWorker(object state) 
     { 
      var si = state as StateInfo; 

      if (si == null) 
      { 
       throw new ArgumentNullException("state"); 
      } 

      si.ExecutionCounter++; 

      if (si.ExecutionCounter > this.Treshold) 
      { 
       this.Timer.Change(Timeout.Infinite, Timeout.Infinite); 
       Console.WriteLine("-Timer stop, execution reached treshold {0}", this.Treshold); 
      } 
      else 
      { 
       Console.WriteLine("{0} lapse, Time {1}", si.ExecutionCounter, si.ToString()); 
      } 
     } 

     public class StateInfo 
     { 
      public int ExecutionCounter 
      { 
       get; 
       set; 
      } 

      public DateTime LastRun 
      { 
       get 
       { 
        return DateTime.Now; 
       } 
      } 

      public override string ToString() 
      { 
       return this.LastRun.ToString(); 
      } 
     } 
    } 

    // Result: 
    // 
    // 1 lapse, Time 2015-02-13 01:28:39 AM 
    // 2 lapse, Time 2015-02-13 01:28:41 AM 
    // -Timer stop, execution reached treshold 2 
    // 
+0

Ich würde vorschlagen, einen Codeformatierer oder etwas zu verwenden, um diesen Code, re. Abstand, Formatierung usw. Es mag wunderbar sein, aber der erste Eindruck zählt, und mein erster Eindruck, wenn ich nicht Code verwenden möchte, der so aussieht. – ProfK

+0

Es war ein schnelles aber funktionierendes Beispiel, hoffe es ist jetzt besser lesbar. – BTE