Im vorherigen Artikel Praxisleitfaden, um unter normalem Windows so weit wie möglich Soft-Realtime zu erreichen – die Checkliste, die man zuerst prüfen sollte haben wir uns damit befasst, periodische Schleifen, die sich auf Sleep verlassen, zu vermeiden und stattdessen ereignisgesteuerte Ansätze sowie waitable Timer zu verwenden. Kurz gesagt lautete das Fazit dieses Artikels: Wenn Sie Jitter der Periode und verpasste Deadlines reduzieren wollen, sollten Sie nicht zuerst die Art des Timers, sondern die „Art des Wartens“ selbst gestalten.
Wie ist das nun in der alltäglicheren .NET-Anwendungsentwicklung?
Hier verwirren sich leicht PeriodicTimer, System.Threading.Timer und DispatcherTimer.
Alle drei heißen „Timer“, aber
- ein Timer, bei dem Sie den Tick per
awaitabwarten - ein Timer, dessen Callbacks auf dem ThreadPool eintreffen
- ein Timer, der auf dem
Dispatcherdes UI-Threads läuft
sind vom Charakter her recht unterschiedlich.
In der Praxis verwechselt man meist Folgendes.
- Man übergibt
System.Threading.Timereineasync-Lambda, obwohl die periodische Verarbeitung asynchron ist - Man greift von einem ThreadPool-Timer direkt auf den Bildschirm zu, obwohl es sich um ein WPF-UI-Update handelt
- Man packt schwere Verarbeitung in einen
DispatcherTimerund verlangsamt damit die ganze Oberfläche - Die vorherige „Soft-Realtime“-Diskussion und die periodische Ausführung einer gewöhnlichen App vermischen sich im Kopf
Dieser Artikel geht überwiegend von allgemeinen C#-/.NET-Anwendungen ab .NET 6 aus und ordnet PeriodicTimer / System.Threading.Timer / DispatcherTimer in einer Reihenfolge, die im Alltag der Praxis möglichst wenig Verwirrung stiftet.
Als Zielgruppe sind dabei etwa diese gemeint:
- Worker / Hintergrunddienste
- Konsolenanwendungen
- Verarbeitung hinter den Kulissen in ASP.NET Core
- WPF-Desktopanwendungen
Mit DispatcherTimer ist in diesem Artikel hauptsächlich WPFs System.Windows.Threading.DispatcherTimer gemeint.
Auch WinUI / UWP besitzen einen DispatcherTimer mit demselben Grundgedanken.
Bei WinForms ist es natürlicher, als UI-Timer System.Windows.Forms.Timer zu betrachten.
Dieser Artikel richtet die Erklärung auf der UI-Seite an WPF aus, schließt WinForms aber mit ein. Ersetzen Sie DispatcherTimer gedanklich durch System.Windows.Forms.Timer, gelten die Hinweise aus 4.3 und 5.2 unverändert. Dabei unterscheiden sich nur diese zwei Punkte:
System.Windows.Forms.Timerist ein Single-Thread-Timer, bei dem Tick über die Nachrichtenschleife ausgelöst wird; eine Priorität wie beiDispatcherTimer(DispatcherPriority) gibt es nicht- Laut Microsoft-Dokumentation liegt die Genauigkeit bei rund 55 Millisekunden. Für feine Perioden eignet er sich daher nicht; in diesem Fall sollten Sie etwas anderes als einen UI-Timer in Betracht ziehen
Zu beachten ist außerdem: Hier geht es um die Frage, wie man die periodische Ausführung auf Anwendungsseite schreibt. Wenn die Genauigkeit der Periode selbst das Thema ist, kehren wir zum vorherigen Soft-Realtime-Artikel zurück.
Der in diesem Artikel gezeigte Code ist außerdem als vollständiges, build- und lauffähiges Beispielpaket auf GitHub veröffentlicht (Bibliotheken und Konsolen-Demos zu PeriodicTimer / System.Threading.Timer sowie Unit-Tests, die das Zusammenfallen von Ticks und die Überlappung von Callbacks überprüfen).
periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)
Inhaltsverzeichnis
- Zuerst das Fazit (in einem Satz)
- Erst einmal auf einen Blick
- 2.1. Das Gesamtbild
- 2.2. Die erste Entscheidungstabelle
- Was Sie zuerst unterscheiden sollten
- 3.1. Callback-Typ oder Typ, der auf Tick wartet?
- 3.2. Läuft es auf dem ThreadPool oder auf dem UI-Thread?
- 3.3. Periodische Verarbeitung und Genauigkeitsgarantie sind getrennte Themen
- Typische Muster
- 4.1. Für asynchrone periodische Verarbeitung:
PeriodicTimer - 4.2. Für leichte Callbacks auf dem ThreadPool:
System.Threading.Timer - 4.3. Für WPF-UI-Updates:
DispatcherTimer - 4.4. Für eher soft-realtime-artige periodische Verarbeitung: ein anderes Werkzeug ansehen
- 4.1. Für asynchrone periodische Verarbeitung:
- Häufige Antipatterns
- Checkliste fürs Review
- Grobe Entscheidungshilfe
- Zusammenfassung
- Quellen
1. Zuerst das Fazit (in einem Satz)
- Wollen Sie eine Verarbeitung in festen Abständen natürlich auf
await-Basis schreiben, greifen Sie zuerst zuPeriodicTimer - Wollen Sie leichte Callbacks periodisch auf dem ThreadPool auslösen, verwenden Sie
System.Threading.Timer - Wollen Sie auf WPFs UI-Thread die Oberfläche aktualisieren, verwenden Sie
DispatcherTimer - Bei
System.Threading.Timerkönnen sich Callbacks überlappen. Wird asynchrone Verarbeitung dort nachlässig hineingestopft, wird es schnell unübersichtlich DispatcherTimererlaubt den direkten Zugriff auf die UI, aber im Gegenzug bremst schwere Verarbeitung darin leicht die gesamte Oberfläche aus- Im Kontext des vorherigen Soft-Realtime-Artikels sind diese drei nicht die Hauptdarsteller für hochpräzises Warten
Kurz gesagt sollten Sie zuerst diese drei Dinge betrachten:
- Auf welchem Thread / in welchem Kontext soll es laufen?
- Wollen Sie den Verarbeitungskörper durchgängig mit
async/awaitschreiben? - Können Sie überlappende Callbacks tolerieren?
Allein durch die Trennung dieser drei Punkte wird die Entscheidung deutlich weniger verwirrend.
2. Erst einmal auf einen Blick
2.1. Das Gesamtbild
flowchart LR
A["Etwas in festen Abständen tun"] --> B{"Soll es auf dem UI-Thread laufen?"}
B -- "Ja" --> C["DispatcherTimer"]
B -- "Nein" --> D{"Soll der Verarbeitungskörper<br/>direkt mit<br/>async / await geschrieben werden?"}
D -- "Ja" --> E["PeriodicTimer"]
D -- "Nein" --> F{"Soll ein leichter<br/>Callback auf dem<br/>ThreadPool laufen?"}
F -- "Ja" --> G["System.Threading.Timer"]
F -- "Nein" --> H["Anderes Design prüfen<br/>Channel / BackgroundService / Event / waitable Timer"]
In der Praxis reicht diese Verzweigung meist aus. Im Zweifel liegen Sie am seltensten falsch, wenn Sie zuerst so trennen:
PeriodicTimer für asynchrone Verarbeitung, DispatcherTimer für UI-Updates.
System.Threading.Timer ist praktisch, hat aber mit der Überlappung von Callbacks und den Eigenheiten der Lebensdauerverwaltung einige Tücken, weshalb er als allererste Wahl etwas anspruchsvoll ist.
2.2. Die erste Entscheidungstabelle
| Situation | Erste Wahl | Wo es läuft | Warum es passt | Erste Vorsicht |
|---|---|---|---|---|
| In festen Abständen async-Verarbeitung wie HTTP / DB / Datei-I/O ausführen | PeriodicTimer |
Innerhalb des Ablaufs der aktuellen async-Methode | Auf await-Basis schreibbar, Stoppen und Abbrechen sind natürlich |
Auf Basis von einem Timer, einem Konsumenten. Verzögerungen werden nicht automatisch parallelisiert |
| Leichte Heartbeats / Metrikversand / Ablaufprüfungen des Caches auf dem ThreadPool ausführen | System.Threading.Timer |
ThreadPool | Leichtgewichtig und Callback-basiert. Lässt sich leicht in ein bestehendes Callback-Design einfügen | Callbacks müssen reentrant sein. Überlappung möglich. Referenz muss gehalten werden |
| Uhrzeitanzeige oder leichte UI-Updates in WPF in festen Abständen ausführen | DispatcherTimer |
WPFs Dispatcher (UI-Thread) |
UI direkt anfassbar. Besitzt Prioritäten | Exakte Auslösezeitpunkte nicht garantiert. Schwere Verarbeitung blockiert die UI |
Die Genauigkeit der Periode ist der eigentliche Punkt, Sleep-basiertes Warten soll vermieden werden |
Diese drei nicht in den Vordergrund stellen | – | Ziel ist nicht die periodische Ausführung der App, sondern das Design der Wartepräzision | Event / waitable Timer betrachten |
Wichtig an dieser Tabelle ist, nicht auf den Timer-Namen zu schauen, sondern auf Ausführungsort und Schreibweise. Wenn die Timer-Wahl schiefgeht, liegt es häufiger daran, dass man nicht auf „wo es läuft“ geachtet hat, als am API-Namen.
3. Was Sie zuerst unterscheiden sollten
3.1. Callback-Typ oder Typ, der auf Tick wartet?
Wird das getrennt, wird der Überblick auf einen Schlag klarer.
System.Threading.TimerundDispatcherTimersind Callback-/Event-TypenPeriodicTimerist der Typ, bei dem Sie den Tick perawaitabwarten
Anders gesagt:
- Callback-Typ: „Der Timer ruft mich an“
PeriodicTimer: „Ich warte auf den nächsten Tick“
Ist der Verarbeitungskörper async und wollen Sie „warten → verarbeiten → wieder warten“ als einen durchgehenden Ablauf lesen, ist PeriodicTimer die natürlichere Wahl.
Umgekehrt passt System.Threading.Timer, wenn:
- Sie sich in ein bestehendes Callback-basiertes Design einfügen wollen
- der Verarbeitungskörper kurz und synchron ist
- Sie einfach einen periodischen Kick auslösen wollen
PeriodicTimer ist praktisch, aber kein Allheilmittel.
Er ist nicht dafür ausgelegt, dass gegen denselben Timer gleichzeitig mehrere WaitForNextTickAsync-Aufrufe laufen, und wenn während der Zeit, in der Sie nicht warten, mehrere Ticks auftreten, werden diese zu einem einzigen zusammengefasst.
Wichtig ist, dies nicht als „holt automatisch auf“ misszuverstehen.
3.2. Läuft es auf dem ThreadPool oder auf dem UI-Thread?
Als Nächstes sollten Sie betrachten, wo die Ausführung stattfindet.
Der Callback von System.Threading.Timer läuft nicht auf dem erstellenden Thread, sondern auf dem ThreadPool.
Das eignet sich zwar für Hintergrundverarbeitung, ist aber nicht dafür gedacht, die UI direkt anzufassen.
DispatcherTimer hingegen ist ein UI-Timer, der in die Dispatcher-Warteschlange integriert ist.
In WPF läuft er auf demselben Dispatcher, sodass Sie im Tick-Handler die UI direkt aktualisieren können.
Dieser Unterschied ist erheblich.
- Um von einem ThreadPool-Timer aus die UI anzufassen, müssen Sie explizit zur UI zurückwechseln
DispatcherTimermacht den Zugriff auf die UI leicht, verbraucht dafür aber entsprechend Zeit auf dem UI-Thread
DispatcherTimers Stärke ist also, dass man „die UI sicher anfassen“ kann, was gleichzeitig bedeutet, dass „schwere Verarbeitung darin auch Eingabe und Neuzeichnen mit hineinzieht“.
3.3. Periodische Verarbeitung und Genauigkeitsgarantie sind getrennte Themen
Dieser Punkt ist als Anknüpfung an den vorherigen Artikel wichtig.
Auch wenn die Formulierung „etwas in festen Abständen tun“ dieselbe ist,
- als Bequemlichkeit der App alle paar Sekunden etwas periodisch zu tun, und
- im Bereich von 1 ms bis wenigen ms so nah wie möglich an eine Deadline heranzukommen,
sind unterschiedliche Probleme.
System.Threading.Timer ist ein leichtgewichtiger, einfach zu handhabender Timer, aber kein dediziertes Werkzeug für Genauigkeit.
Auch DispatcherTimer wird von den Umständen der Dispatcher-Warteschlange und von Prioritäten beeinflusst.
Auch PeriodicTimer wirkt allein vom Namen her so, als sei „die Periode besonders exakt“, aber seine praktische Stärke liegt weniger in der Präzision als in der Schreibbarkeit des async-Ablaufs.
Es ist daher sicherer, von vornherein zu trennen, ob Sie
- die periodische Ausführung der App schreiben wollen, oder
- die Wartepräzision verfeinern wollen.
Vermischen sich diese beiden, driftet die Diskussion über die Timer-Wahl allmählich in eine seltsame Richtung.
4. Typische Muster
4.1. Für asynchrone periodische Verarbeitung: PeriodicTimer
Wollen Sie in einem Worker, einem BackgroundService oder einem dauerhaft laufenden Konsolenprozess in festen Abständen asynchrone Verarbeitung ausführen, lässt sich das zuerst am einfachsten mit PeriodicTimer schreiben.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class CacheRefreshWorker : BackgroundService
{
private readonly ILogger<CacheRefreshWorker> _logger;
public CacheRefreshWorker(ILogger<CacheRefreshWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("CacheRefreshWorker started.");
await RefreshCacheAsync(stoppingToken);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RefreshCacheAsync(stoppingToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("CacheRefreshWorker stopping.");
}
}
private async Task RefreshCacheAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Refreshing cache...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
Die Vorzüge dieser Form:
- Der Codeablauf lässt sich als eine einzige
async-Methode gut verfolgen - Das
CancellationTokenlässt sich direkt nach unten weiterreichen - Weniger Callback-basierte Lebensdauer- und Ausnahmeverwaltung
Besonders gut passt das, wenn der Verarbeitungskörper
- HTTP aufruft
- eine DB abfragt
- eine Datei liest
- eine andere async-API awaitet
also im Wesentlichen auf I/O-Wartezeiten beruht.
Es gibt zwei Vorsichtspunkte.
- Auf Basis von einem Timer, einem Konsumenten verwenden
- Selbst die Strategie festlegen, wenn die Verarbeitungszeit länger ist als die Periode
PeriodicTimer parallelisiert nicht automatisch, um aufzuholen, nur weil die vorherige Verarbeitung länger gedauert hat. In diesem Sinne ist er ein Timer, um „eine asynchrone Schleife in festen Abständen natürlich zu schreiben“.
Wer auch die Testbarkeit im Blick hat: Es gibt auch einen Konstruktor, der einen TimeProvider entgegennimmt, was praktisch ist.
4.2. Für leichte Callbacks auf dem ThreadPool: System.Threading.Timer
Wollen Sie lediglich periodisch einen kurzen Callback aufrufen, ist System.Threading.Timer unkompliziert.
Zum Beispiel bei:
- einem Heartbeat
- dem Erfassen leichter Metriken
- einer kurzen Ablaufprüfung
- dem Einhängen in ein bestehendes Callback-basiertes Design
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class HeartbeatService : IHostedService, IDisposable
{
private readonly ILogger<HeartbeatService> _logger;
private Timer? _timer;
private int _running;
public HeartbeatService(ILogger<HeartbeatService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void OnTimer(object? state)
{
if (Interlocked.Exchange(ref _running, 1) != 0)
{
return;
}
try
{
_logger.LogInformation("Heartbeat: {Now}", DateTimeOffset.Now);
}
finally
{
Volatile.Write(ref _running, 0);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Dieses Beispiel enthält Interlocked.Exchange, weil System.Threading.Timer nicht auf den Abschluss des vorherigen Callbacks wartet.
Das ist ziemlich wichtig.
- Callbacks laufen auf dem ThreadPool
- Callbacks müssen reentrant sein
- Ist die Verarbeitung länger als das Intervall, können sie sich überlappen
Diese „mögliche Überlappung“ lässt sich in den Unit-Tests des Beispiels tatsächlich beobachten. Bei einem Timer mit 50 ms Periode und einer Verarbeitung, die 300 ms dauert, steigt die maximale Zahl gleichzeitiger Ausführungen auf 2 oder mehr. In der Version mit derselben Interlocked.Exchange-Absicherung wie oben bleibt die maximale Zahl gleichzeitiger Ausführungen unter denselben Bedingungen bei 1, stattdessen werden während der Ausführung ausgelöste Callbacks übersprungen.
// Auszug aus tests/KomuraSoft.TimerSelection.Tests/ThreadPoolTimerOverlapTests.cs
// Ohne Absicherung: Periode 50 ms, Verarbeitung 300 ms. Warten, bis eine Überlappung beobachtet wird
bool overlapped = await WaitUntilAsync(
() => Volatile.Read(ref maxObserved) >= 2,
TimeSpan.FromSeconds(10));
Assert.True(overlapped, "timer callbacks did not overlap within the timeout.");
// Mit Absicherung: Die maximale Zahl gleichzeitiger Ausführungen bleibt unter denselben Bedingungen bei 1
Assert.Equal(1, Volatile.Read(ref maxConcurrent));
Ist die Verarbeitung nicht leicht, ist es ruhiger, sie so zu gestalten, dass Sie:
- doppelte Aufrufe überspringen
- in eine Warteschlange einreihen
- auf
PeriodicTimerumsteigen
Ein weiterer, unauffällig wichtiger Punkt: die Referenz halten. System.Threading.Timer wird auch während des Laufens zum GC-Kandidaten, sobald keine Referenz mehr existiert. Auch direkt nach dem Aufruf von Dispose() kann ein bereits eingereihter Callback noch nachträglich ausgeführt werden.
System.Threading.Timer ist also
- leichtgewichtig
- schnell
- einfach
aber im Gegenzug ein Timer, bei dem Sie sich selbst ordentlich um die Belange des Callbacks kümmern müssen.
4.3. Für WPF-UI-Updates: DispatcherTimer
Wollen Sie in WPF eine Uhr auf dem Bildschirm oder eine leichte Statusanzeige periodisch aktualisieren, ist DispatcherTimer naheliegend.
using System;
using System.Windows;
using System.Windows.Threading;
public partial class MainWindow : Window
{
private readonly DispatcherTimer _clockTimer;
public MainWindow()
{
InitializeComponent();
_clockTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromSeconds(1)
};
_clockTimer.Tick += ClockTimer_Tick;
_clockTimer.Start();
}
private void ClockTimer_Tick(object? sender, EventArgs e)
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
}
protected override void OnClosed(EventArgs e)
{
_clockTimer.Stop();
_clockTimer.Tick -= ClockTimer_Tick;
base.OnClosed(e);
}
}
Das im Konstruktor übergebene DispatcherPriority.Background gibt an, mit welcher Priorität der Dispatcher-Warteschlange der Tick verarbeitet wird. Auch das parameterlose new DispatcherTimer() verwendet standardmäßig Background; hier wird also nur der Standardwert explizit gemacht, ohne das Verhalten zu ändern. Background (Wert 4) ist die Priorität „verarbeiten, nachdem alle anderen Nicht-Leerlauf-Vorgänge abgeschlossen sind“ und liegt niedriger als Input (5) oder Render (7). Das bedeutet: Tick wird nicht auf Kosten von Eingabeverarbeitung oder Rendering ausgeführt. Für eine Verwendung wie die Uhrzeitanzeige, bei der „eine kleine Abweichung nichts ausmacht, aber die Bedienung nicht gestört werden soll“, passt das gut. Wollen Sie das Ergebnis des Ticks so schnell wie möglich auf dem Bildschirm sehen, können Sie auch bis Normal (9) hochgehen — dann müssen Sie aber umso strenger darauf achten, den Inhalt des Ticks leicht zu halten.
Der Vorzug von DispatcherTimer ist, dass Tick auf WPFs Dispatcher verarbeitet wird und Sie deshalb die UI direkt anfassen können.
Das passt zum Beispiel gut zu:
- einer Uhrzeitanzeige
- einer leichten Aktualisierung der Verbindungsstatusanzeige
- dem Auslöser für die Neubewertung eines Command
- einer leichten Aktualisierung von auf dem Bildschirm angezeigten Zahlen
Aber auch hier gibt es einen Punkt, an dem sich die Lage ändert.
DispatcherTimer läuft auf dem UI-Thread, sodass schwere Verarbeitung im Tick-Handler direkt Eingabe, Rendering und Layout mit ausbremst.
Außerdem ist DispatcherTimer kein Werkzeug, das „exakt zum angegebenen Zeitpunkt“ garantiert. Er wird von anderer Arbeit in der Dispatcher-Warteschlange und von Prioritäten beeinflusst.
In der Praxis bleibt es also stabil, wenn Sie im Kopf behalten:
- den Inhalt des Ticks leicht zu halten
- schwere I/O oder CPU-Arbeit an anderer Stelle auszulagern
- beim Schließen
Stop()aufzurufen und die Ereignisbehandlung abzumelden, um die Lebensdauer explizit zu machen
4.4. Für eher soft-realtime-artige periodische Verarbeitung: ein anderes Werkzeug ansehen
Das ist der Anknüpfungspunkt an den vorherigen Artikel.
Worum es im vorherigen Soft-Realtime-Artikel ging, war nicht „ungefähr alle paar Sekunden reicht“, sondern wie man Jitter der Periode und verpasste Deadlines reduziert.
In diesem Kontext geht es um:
- kein
Sleep-basiertes relatives Warten - den Einsatz ereignisgesteuerter Ansätze und waitabler Timer
- die Trennung von Fast Path und Slow Path
- das Messen von Verzögerungen
Es ist daher am saubersten, das Problem von vornherein so zu trennen:
- alltägliche asynchrone periodische Verarbeitung in einer App
→
PeriodicTimer - ThreadPool-Callback
→
System.Threading.Timer - UI-Update
→
DispatcherTimer - die Genauigkeit der Periode selbst steht im Vordergrund → die Welt des vorherigen Artikels
Die Frage „Ich will so exakt wie möglich alle 1 ms laufen — welcher .NET-Timer ist gut?“ ist zur Hälfte gar keine Timer-Wahl mehr, sondern eine Frage der Wartemethode und des Designs.
5. Häufige Antipatterns
5.1. System.Threading.Timer direkt eine async-Lambda übergeben
Das passiert ziemlich leicht.
_timer = new Timer(async _ => await RefreshAsync(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
Optisch wirkt das aufgeräumt, aber TimerCallback ist void.
Das heißt, diese async-Lambda wird faktisch wie async void behandelt.
Dadurch entsteht ein schwer handhabbarer Zustand:
- Die aufrufende Seite kann nicht awaiten
- Der Abschluss lässt sich nicht abwarten
- Das Ausnahme-Management wird schwierig
- Die Überlappung von Callbacks muss zusätzlich bedacht werden
Warum das Ausnahme-Management schwierig wird, ist eine genauere Erklärung wert. Bei async Task liegt die Ausnahme im Task, sodass die aufrufende Seite sie beim await erhält. Bei async void (bzw. dessen Entsprechung) gibt es diesen Task nicht, weshalb eine geworfene Ausnahme direkt an den SynchronizationContext erneut geworfen wird, der beim Start dieser Methode gültig war. Der Callback von System.Threading.Timer läuft auf dem ThreadPool, wo es keinen SynchronizationContext gibt. Die Ausnahme wird dadurch zu einer unbehandelten Ausnahme auf einem ThreadPool-Thread, was standardmäßig den gesamten Prozess beendet. Sofern Sie den Inhalt des Callbacks nicht selbst mit try / catch umschließen, lässt sich das von außen nicht auffangen.
Ist der Verarbeitungskörper async, ist es lesbarer, zunächst PeriodicTimer in Betracht zu ziehen.
5.2. Schwere Verarbeitung in den Tick von DispatcherTimer legen
Da DispatcherTimer die UI direkt anfassen lässt, ist die Versuchung groß, alles Mögliche dort hineinzuschreiben.
Das ist aber der UI-Thread.
- lange synchrone Verarbeitung
- schwere CPU-Berechnung
- blockierende I/O
- Verarbeitung mit langem
await, die doppelt ausgelöst werden kann
führt zu einem direkten Zusammenstoß mit Eingabe und Rendering der UI.
Stabiler ist es, den Inhalt des Ticks leicht zu halten, schwere Arbeit in den Hintergrund auszulagern und nur das benötigte Ergebnis an die UI zurückzugeben.
5.3. Glauben, PeriodicTimer würde eine Verzögerung automatisch aufholen
Auch das ist ein leicht entstehendes Missverständnis.
PeriodicTimer ist als Werkzeug hervorragend, um eine asynchrone Schleife in festen Abständen sauber zu schreiben, aber er parallelisiert nicht automatisch, um aufzuholen, wenn die vorherige Verarbeitung länger gedauert hat.
Das lässt sich auch mit den Unit-Tests des Beispiels nachvollziehen. Lässt man einen Timer mit 250 ms Periode 1,5 Sekunden lang unbeobachtet und wartet dann, ist die erste Wartung sofort abgeschlossen — dank der aufgelaufenen Ticks —, die zweite jedoch nicht. Das bedeutet: Die mehreren Ticks während der unbeobachteten Zeit wurden zu einem einzigen zusammengefasst.
// Auszug aus tests/KomuraSoft.TimerSelection.Tests/PeriodicTimerBehaviorTests.cs
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(250));
await Task.Delay(TimeSpan.FromMilliseconds(1500)); // In dieser Zeit wartet niemand
// Die erste Wartung ist dank des aufgelaufenen Ticks sofort abgeschlossen
ValueTask<bool> first = timer.WaitForNextTickAsync();
Assert.True(first.IsCompleted);
Assert.True(await first);
// Die zweite Wartung ist nicht sofort abgeschlossen (es sind keine weiteren Ticks übrig)
ValueTask<bool> second = timer.WaitForNextTickAsync();
Assert.False(second.IsCompleted);
Da Ticks während der unbeobachteten Zeit zu einem einzigen zusammengefasst werden können, müssen Sie im Design festlegen, ob
- bei Verzögerung übersprungen wird
- nur der aktuellste Zustand relevant ist
- wirklich jede einzelne Ausführung verarbeitet werden muss
5.4. Stoppen und Lebensdauerverwaltung auf später verschieben
Bei Timern passieren eher beim Stoppen als beim Laufen Fehler.
Leicht zu übersehen sind unter anderem:
System.Threading.Timerals lokale Variable anzulegen, ohne die Referenz zu haltenSystem.Threading.Timernicht zu stoppen und den Umgang mitDispose()unklar zu lassenDispatcherTimerweder mitStop()anzuhalten noch das Tick-Abonnement zu entfernen- dass der Timer auch nach dem Schließen des Fensters die Lebensdauer eines Objekts verlängert
Insbesondere DispatcherTimer kann das Objekt, an das die Methode gebunden ist, am Leben halten. Bei dem eigenartigen Gefühl „Dieses Window müsste doch geschlossen sein, hängt aber irgendwie noch herum“ lohnt es sich, hier nachzusehen.
6. Checkliste fürs Review
- Können Sie erklären, ob diese periodische Verarbeitung als UI-Update / ThreadPool-Callback / async-Schleife geschrieben werden sollte?
- Wird ein async-Verarbeitungskörper gewaltsam in einen Callback-Timer gepresst, obwohl er das nicht sein sollte?
- Verwenden Sie
System.Threading.Timer: Verträgt der Code eine Überlappung von Callbacks, oder ist sie abgesichert? - Enthält der Tick von
DispatcherTimerschwere Verarbeitung, blockierende I/O oder lange synchrone Verarbeitung? - Verwenden Sie
PeriodicTimer: Ist die Strategie für den Fall einer Verzögerung festgelegt? - Sind Stoppmethode (
Change/Dispose/Stop) und der Ablauf beim Beenden der App klar? - Wird die Referenz auf
System.Threading.Timerordentlich gehalten? - Gibt es für
DispatcherTimerbeim Schließen des Fensters ein Abmelden des Abonnements und ein Aufräumen? - Ist von vornherein getrennt, ob es sich bei dem Problem um „periodische Ausführung der App“ oder um „Wartepräzision“ handelt?
7. Grobe Entscheidungshilfe
Hier ein paar praktische Faustregeln.
-
Alle 30 Sekunden eine API aufrufen, um Einstellungen zu aktualisieren →
PeriodicTimer -
Alle 5 Sekunden einen Heartbeat oder leichte Metriken senden →
System.Threading.Timer -
In WPF eine Uhrzeitanzeige oder leichte Statusaktualisierungen anzeigen →
DispatcherTimer -
Bei jedem Tick direkt die UI anfassen →
DispatcherTimer -
Der Körper der periodischen Verarbeitung besteht überwiegend aus
await, und Stoppen sowie Ausnahmen sollen natürlich behandelt werden →PeriodicTimer -
Einen callback-basierten kleinen Kick kostengünstig einbauen →
System.Threading.Timer -
Periodengenauigkeit und Jitter im Bereich von 1–5 ms sind der eigentliche Kern → vor diesen dreien die Wartemethoden aus dem vorherigen Artikel ansehen
Ziemlich grob in je einem Satz gesagt:
PeriodicTimerist der Timer für asyncSystem.Threading.Timerist der Timer für ThreadPool-CallbacksDispatcherTimerist der Timer für die UI
Mit dieser Eselsbrücke liegen Sie selten weit daneben.
8. Zusammenfassung
Was bei der Wahl eines .NET-Timers wirklich zählt, ist nicht der Unterschied im Namen, sondern diese drei Punkte.
- Wo es läuft
- In welchem Ablauf Sie es schreiben wollen
- Wie Sie mit Überlappung und Verzögerung umgehen
Als Strategie reicht allein das schon aus, um gut zurechtzukommen.
- Asynchrone periodische Verarbeitung:
PeriodicTimer - Leichte Callbacks auf dem ThreadPool:
System.Threading.Timer - WPF-UI-Updates:
DispatcherTimer - Genauigkeit im Vordergrund: eine andere Wartemethode ansehen
Timer verschwimmen leicht, weil die Namen ähnlich klingen. Ihre Rollen sind aber gar nicht so ähnlich.
PeriodicTimerist ein Werkzeug, um den async-Ablauf zu gestaltenSystem.Threading.Timerist ein Werkzeug, um Callbacks periodisch anzustoßenDispatcherTimerist ein Werkzeug für periodische Updates auf dem UI-Thread
Allein diese drei getrennt zu betrachten, macht den Code deutlich ruhiger.
Vermischt sich das hingegen, passiert recht gewöhnlicher, aber lästiger Ärger:
- was async sein sollte, wird
async void-artig - der Absturz durch direkten UI-Zugriff
- Callbacks überlappen sich und der Zustand wird trüb
- die Diskussion um Periodengenauigkeit wird mit hineingezogen
Beginnen Sie damit, zu betrachten, „wo Sie es laufen lassen wollen“. Allein das macht die Timer-Wahl deutlich gelassener.
9. Quellen
- Vollständiger Beispielcode zu diesem Artikel (Bibliothek, Demo, Unit-Tests) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/periodictimer-system-threading-timer-dispatchertimer-guide
- Verwandter Artikel: Praxisleitfaden, um unter normalem Windows so weit wie möglich Soft-Realtime zu erreichen – die Checkliste, die man zuerst prüfen sollte
- Verwandter Artikel: C# async/await-Praxis-Entscheidungstabelle – Task.Run und ConfigureAwait
- Verwandter Artikel: WPF/WinForms – async und der UI-Thread auf einen Blick
- Timers - .NET
- PeriodicTimer Class
- PeriodicTimer.WaitForNextTickAsync(CancellationToken) Method
- PeriodicTimer.Dispose Method
- PeriodicTimer Constructor
- Timer Class (System.Threading)
- Timer Constructor (System.Threading)
- Background tasks with hosted services in ASP.NET Core
- DispatcherTimer Class (System.Windows.Threading)
- DispatcherTimer Class (Microsoft.UI.Xaml)
- DispatcherTimer Constructor (Standard ist die Priorität Background)
- DispatcherPriority Enum
- Timer Class (System.Windows.Forms) (Genauigkeit rund 55 Millisekunden)
- Async/Await - Best Practices in Asynchronous Programming (Umgang mit Ausnahmen bei async void)
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Warum .NET Generic Host und BackgroundService in Desktop-Apps verwenden
Wie sich Generic Host und BackgroundService nutzen lassen, um Start, periodische Verarbeitung, Beendigung, Logging, Konfiguration und DI ...
Praktische Best Practices für Multithreading: .NET-Edition — Was Sie entscheiden sollten, bevor Sie weitere Threads hinzufügen
Eine praxisnahe Übersicht der Entwurfsregeln, die verhindern, dass Multithreading-Code in .NET/C# gelegentlich abstürzt oder hängen bleib...
Code-Design für Geschäftssysteme — Produkt- und Kundencodes festlegen, und Prüfziffern
Ein praxisnaher Leitfaden zur Festlegung des Codesystems eines Geschäftssystems, einschließlich Produkt- und Kundencodes. Behandelt eine ...
CI/CD für WinForms-/WPF-Anwendungen in der Praxis ── Vom Build über die Signierung bis zur Distribution mit GitHub Actions automatisieren
Ein praktischer Leitfaden zur Einrichtung von CI/CD für WinForms-/WPF-Anwendungen mit GitHub Actions. Behandelt eine minimale YAML für Bu...
Energiesparmodus, Ruhezustand, Modern Standby und lang laufende Anwendungen ── Design gegen „über Nacht stehengeblieben“
Warum eine lang laufende Windows-Anwendung morgens beim Nachsehen „stehengeblieben“ sein kann, aufgearbeitet anhand der Unterschiede zwis...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
UI-Threading und Timer
WPF-/WinForms-UI-Thread, asynchrone Abläufe, Dispatcher und Timer-Entscheidungen.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Bei der Windows-Anwendungsentwicklung mit periodischer Ausführung, UI-Updates und Hintergrundverarbeitung wirkt sich die Timer-Wahl unmittelbar auf die Implementierungsqualität aus.
Technische Beratung und Design-Review
Wenn Sie sich in der Phase befinden, in der Sie klären möchten, wo die Zuständigkeiten von PeriodicTimer und DispatcherTimer zu trennen sind, lässt sich das als technische Beratung und Design-Review angehen.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was ist der Unterschied zwischen PeriodicTimer und System.Threading.Timer?
- Der größte Unterschied ist, dass PeriodicTimer ein Typ ist, bei dem Sie den Tick per await abwarten, während System.Threading.Timer nach dem Callback-Prinzip arbeitet. Bei PeriodicTimer lässt sich „warten → verarbeiten → wieder warten“ als durchgehender Ablauf in einer einzigen async-Methode schreiben, und ein CancellationToken lässt sich leicht nach unten weiterreichen. Der Callback von System.Threading.Timer läuft dagegen auf dem ThreadPool und wartet nicht auf den Abschluss des vorherigen Callbacks, sodass sich Aufrufe überlappen können, wenn die Verarbeitung länger dauert als das Intervall. Für asynchrone periodische Verarbeitung eignet sich PeriodicTimer, für einen leichten, synchronen periodischen Callback-Kick System.Threading.Timer.
- Holt PeriodicTimer automatisch auf, wenn die Verarbeitung in Verzug gerät?
- Nein. Nur weil die vorherige Verarbeitung länger gedauert hat, führt PeriodicTimer nicht automatisch parallele Läufe aus, um aufzuholen. Auch wenn während der Zeit, in der Sie nicht warten, mehrere Ticks auftreten, werden diese zu einem einzigen zusammengefasst. Deshalb müssen Sie selbst im Design festlegen, ob bei Verzögerung übersprungen wird, nur der aktuellste Zustand relevant ist oder wirklich jede einzelne Ausführung verarbeitet werden muss. Außerdem ist PeriodicTimer nicht dafür ausgelegt, dass mehrere WaitForNextTickAsync-Aufrufe gleichzeitig gegen denselben Timer laufen.
- Darf ich System.Threading.Timer keine async-Lambda übergeben?
- Das sollten Sie besser vermeiden. Da TimerCallback vom Typ void ist, wird eine übergebene async-Lambda faktisch wie async void behandelt. Die aufrufende Seite kann dann nicht awaiten, den Abschluss nicht abwarten, das Ausnahme-Management wird schwieriger, und die Überlappung von Callbacks muss zusätzlich bedacht werden. Wenn der eigentliche Verarbeitungskörper async ist, ist es lesbarer und sicherer, zunächst PeriodicTimer in Betracht zu ziehen.
- Wann sollte ich DispatcherTimer verwenden?
- Verwenden Sie ihn, wenn Sie in WPF etwas wie eine Uhrzeitanzeige oder eine leichte Statusanzeige regelmäßig aktualisieren möchten. Da Tick auf dem Dispatcher (dem UI-Thread) von WPF verarbeitet wird, liegt die Stärke darin, dass Sie im Handler die UI direkt anfassen können. Weil der Timer aber auf dem UI-Thread läuft, verlangsamt schwere Verarbeitung oder blockierende I/O im Tick auch Eingabe und Rendering. Außerdem ist ein exaktes Auslösen zum angegebenen Zeitpunkt nicht garantiert. Stabil bleibt es, wenn Sie den Inhalt des Ticks leicht halten, schwere Arbeit in den Hintergrund auslagern und beim Schließen des Fensters mit Stop() und dem Abmelden der Ereignisbehandlung die Lebensdauer explizit machen.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.