Menu

C# Timer: System.Threading.Timer, System.Timers.Timer, PeriodicTimer

C# hat mehrere Timer-Klassen, und sie verhalten sich unterschiedlich. Lerne, wann du System.Threading.Timer, System.Timers.Timer, PeriodicTimer oder einen UI-Timer verwendest, wie du sie startest und stoppst, warum Callbacks im Thread-Pool laufen und sich überlappen können, warum ein Timer referenziert bleiben muss und wie du mit Stopwatch vergangene Zeit misst.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein Timer führt Code nach einer Verzögerung oder in einem wiederkehrenden Intervall aus: jede Minute einen Cache auffrischen, alle 30 Sekunden einen Entwurf speichern, einen Dienst abfragen, eine Uhr aktualisieren. .NET hat mehrere Timer-Klassen, die sich ähneln, sich aber darin unterscheiden, wo der Callback läuft und wie du sie steuerst. Diese Seite behandelt jede davon, plus Stopwatch, um zu messen, wie lange etwas gedauert hat.

Welchen Timer du verwendest

TimerCallback läuft aufStilVerwende für
System.Threading.TimerThread-PoolCallback-DelegateHintergrundarbeit in Diensten und Bibliotheken
System.Timers.TimerThread-Pool (standardmäßig)Event Elapsed, Start/StopCode im Komponentenstil, der Events mag
PeriodicTimer (.NET 6+)Dort, wo deine async-Schleife läuftawait WaitForNextTickAsync()Async-Code; Ticks überlappen nie
System.Windows.Forms.Timer, WPF DispatcherTimerDem UI-ThreadEvent TickSteuerelemente in Desktop-Apps aktualisieren

Die ersten beiden feuern auf Threads des Thread-Pools, ihre Callbacks können also gleichzeitig mit dem Rest deines Programms laufen und sogar gleichzeitig miteinander. Die UI-Timer laufen auf dem UI-Thread, deshalb können sie Steuerelemente direkt anfassen, und deshalb friert ein langsamer Handler das Fenster ein.

System.Threading.Timer

Der Konstruktor nimmt den Callback, ein Zustandsobjekt, die Verzögerung vor dem ersten Tick und die Periode. Er startet sofort.

Beispielausgabe:

tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks

Zwei Details zählen hier. Main muss warten (done.WaitOne()); ein Konsolenprogramm endet, wenn Main zurückkehrt, und Timer-Callbacks laufen auf Hintergrund-Threads, die den Prozess nicht am Leben halten. Und der Timer liegt in einem statischen Feld: Ein System.Threading.Timer, auf den nichts verweist, kann vom Garbage Collector eingesammelt werden und hört dann stillschweigend auf zu feuern. Das erwischt Leute am häufigsten, wenn ein Timer als lokale Variable in einer Methode erzeugt wird, die zurückkehrt.

Change(dueTime, period) plant den Timer neu; Timeout.Infinite für beide hält ihn an, und ein neues Paar startet ihn wieder. Dispose() stoppt ihn endgültig.

System.Timers.Timer

System.Timers.Timer verpackt denselben Mechanismus in eine ereignisbasierte API: Interval setzen, Elapsed abonnieren, dann Start() und Stop().

Beispielausgabe:

autosave #1
autosave #2
autosave #3
autosave stopped

AutoReset = false erzeugt einen einmaligen Timer: Er feuert einmal und stoppt, und ein erneutes Start() plant den nächsten einzelnen Tick. Enabled = true und false sind dasselbe wie Start() und Stop(). Der volle Name System.Timers.Timer steht hier ausgeschrieben, weil System.Threading ebenfalls einen Timer hat; sind beide Namespaces importiert, ist ein nacktes Timer mehrdeutig und kompiliert nicht.

System.Timers.Timer hat außerdem eine Property SynchronizingObject, die Elapsed in WinForms auf einen UI-Thread umleitet. In der Praxis ist der eigene Timer des UI-Frameworks dafür einfacher.

Callbacks überlappen und laufen auf anderen Threads

Beide Klassen oben feuern im Thread-Pool, planmäßig, egal ob der vorherige Callback fertig ist. Dauert ein Callback 3 Sekunden und beträgt die Periode 1 Sekunde, laufen drei Callbacks gleichzeitig. Daraus ergeben sich zwei Pflichten:

  • Alles, was der Callback anfasst, muss threadsicher sein. Nimm Interlocked für Zähler (wie die Beispiele) und einen Lock für alles Größere.
  • Schütze dich vor Überlappung, wenn es darauf ankommt. Überspringe entweder einen Tick, solange der vorherige noch läuft, oder nimm ein Muster, das nicht überlappen kann.

Eine Sperre zum Überspringen mit Interlocked:

private int running = 0;

private void OnTick(object state)
{
    if (Interlocked.Exchange(ref running, 1) == 1) return; // previous tick still busy
    try
    {
        SyncOrders();   // slow work
    }
    finally
    {
        Volatile.Write(ref running, 0);
    }
}

Exceptions sind die andere Falle. Eine Exception, die aus einem Callback von System.Threading.Timer entkommt, lässt den Prozess abstürzen, während System.Timers.Timer Exceptions aus Elapsed-Handlern verschluckt, sodass der Fehlschlag unbemerkt bleibt. Umschließe den Rumpf eines Timer-Callbacks mit try/catch und protokolliere.

PeriodicTimer und async-Schleifen

In async-Code ist eine Schleife, die zwischen den Durchläufen wartet, der klarste Timer: Das nächste Warten beginnt erst, wenn die Arbeit fertig ist, Ticks können sich also nie überlappen, und Exceptions tauchen dort auf, wo du sie abfangen kannst. .NET 6 hat genau dafür PeriodicTimer hinzugefügt:

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));

while (await timer.WaitForNextTickAsync(cancellationToken))
{
    await SaveDraftAsync();
}

WaitForNextTickAsync gibt false zurück, sobald der Timer freigegeben ist, was die Schleife beendet, und wirft OperationCanceledException, wenn der Token abgebrochen wird. Anders als eine Schleife um Task.Delay hält ein PeriodicTimer einen festen Takt: Dauert die Arbeit 2 Sekunden, kommt der nächste Tick trotzdem 30 Sekunden nach dem vorherigen Tick, nicht 32.

In jeder Version funktioniert dieselbe Form mit Task.Delay, wenn ein Wegdriften keine Rolle spielt:

Beispielausgabe:

poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds

Abbruch ersetzt Stop(): Übergib der Schleife einen Token und brich ihn von außen ab. Mehr zu Cancellation Tokens unter Tasks.

Vergangene Zeit mit Stopwatch messen

Ein Timer plant Code; Stopwatch misst, wie lange Code gedauert hat. Es liest eine hochauflösende Uhr, die nur vorwärts läuft, und ist damit das richtige Werkzeug zum Messen:

Beispielausgabe:

string +=      64 ms
StringBuilder  0 ms
Same length: True
High resolution clock: True

Start, Stop, Reset und Restart steuern es; Elapsed ist ein TimeSpan, und ElapsedMilliseconds und ElapsedTicks liefern rohe Zahlen. Miss nicht mit DateTime.Now: Seine Auflösung kann bis zu 10 bis 15 ms grob sein, und es springt, wenn die Systemuhr angepasst wird. Für ernsthafte Benchmarks, bei denen JIT-Aufwärmen und Garbage Collection einen einzelnen Lauf verfälschen, nimm die Bibliothek BenchmarkDotNet.

Genauigkeit von Timern

Ein Timer-Intervall ist ein Minimum, kein Versprechen. Unter Windows beträgt die Standardauflösung des Systemtimers etwa 15,6 ms, ein Intervall von 10 ms feuert also eher alle 15 oder 16 ms, und auf einem ausgelasteten Rechner kann ein Callback im Thread-Pool noch später starten. Timer passen für „ungefähr alle N Sekunden“. Für präzises Frame-Timing oder Medien nimm dafür gebaute APIs.

Häufige Fehler

  • Ein System.Threading.Timer, der nur in einer lokalen Variable gespeichert ist. Er kann vom Garbage Collector eingesammelt werden und aufhören zu feuern; halte ihn in einem Feld.
  • Main zurückkehren lassen. Der Prozess endet und nimmt den Timer mit.
  • Mehrdeutiges Timer. Sind System.Threading und System.Timers beide importiert, schreibe den vollen Namen.
  • Annehmen, dass Ticks nie überlappen. Timer im Thread-Pool überlappen, wenn die Arbeit langsamer ist als das Intervall.
  • UI-Steuerelemente aus einem Timer im Thread-Pool anfassen. Nimm den Timer des UI-Frameworks oder leite zurück auf den UI-Thread um.
  • Unbehandelte Exceptions in Callbacks. Sie lassen den Prozess abstürzen (Threading.Timer) oder verschwinden (Timers.Timer); fang sie ab und protokolliere.
  • Dispose vergessen. Timer halten Systemressourcen und feuern weiter, bis sie freigegeben werden.

Häufig gestellte Fragen

Welchen Timer sollte ich in C# verwenden?

In async-Code ab .NET 6 PeriodicTimer mit await timer.WaitForNextTickAsync() in einer Schleife. Für einen Callback im Thread-Pool System.Threading.Timer. Für einen ereignisbasierten Timer mit Start, Stop und einem Event Elapsed System.Timers.Timer. In WinForms oder WPF nimm den Timer des UI-Frameworks, damit der Handler auf dem UI-Thread läuft.

Wie führe ich in C# alle paar Sekunden Code aus?

Erzeuge einen Timer mit dem Intervall, zum Beispiel new System.Threading.Timer(_ => Refresh(), null, 0, 5000), um Refresh sofort und dann alle 5 Sekunden aufzurufen, und halte eine Referenz darauf. In async-Code nimm eine Schleife mit PeriodicTimer (.NET 6+) oder await Task.Delay(5000); die Schleifenversion führt nie zwei Ticks gleichzeitig aus.

Wie stoppe ich in C# einen Timer?

Bei System.Timers.Timer rufe Stop() auf (oder setze Enabled = false) und Dispose(), wenn du ihn nicht mehr brauchst. Bei System.Threading.Timer rufe Change(Timeout.Infinite, Timeout.Infinite) auf, um ihn anzuhalten, oder Dispose(), um ihn endgültig zu stoppen. Ein bereits gestarteter Callback kann nach dem Stoppen des Timers noch zu Ende laufen.

Warum hört mein System.Threading.Timer auf zu feuern?

Nichts verweist auf den Timer, also hat der Garbage Collector ihn eingesammelt, und seine Callbacks haben aufgehört. Das passiert, wenn der Timer als lokale Variable in einer Methode erzeugt wird, die zurückkehrt. Speichere ihn in einem Feld, solange er laufen soll.

Wie messe ich in C# vergangene Zeit?

Nimm System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); und lies dann sw.ElapsedMilliseconds oder sw.Elapsed. Es verwendet eine hochauflösende, monotone Uhr, anders als das Subtrahieren zweier DateTime.Now-Werte, das gröber auflöst und springt, wenn die Systemuhr angepasst wird.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S