Timer uruchamia kod po opóźnieniu albo w powtarzającym się interwale: odświeża cache co minutę, zapisuje szkic co 30 sekund, odpytuje usługę, aktualizuje zegar. .NET ma kilka klas timerów, które wyglądają podobnie, ale różnią się tym, gdzie działa callback i jak nimi sterujesz. Ta strona omawia każdą z nich oraz Stopwatch do mierzenia, ile coś trwało.
Którego timera użyć
| Timer | Callback działa w | Styl | Zastosowanie |
|---|---|---|---|
System.Threading.Timer | Puli wątków | Delegat callbacku | Praca w tle w usługach i bibliotekach |
System.Timers.Timer | Puli wątków (domyślnie) | Zdarzenie Elapsed, Start/Stop | Kod w stylu komponentów, oparty na zdarzeniach |
PeriodicTimer (.NET 6+) | Tam, gdzie działa twoja pętla asynchroniczna | await WaitForNextTickAsync() | Kod asynchroniczny; tyknięcia nigdy się nie nakładają |
System.Windows.Forms.Timer, DispatcherTimer z WPF | Wątku interfejsu | Zdarzenie Tick | Aktualizacja kontrolek w aplikacjach desktopowych |
Pierwsze dwa uruchamiają się w wątkach z puli, więc ich callbacki mogą działać jednocześnie z resztą programu, a nawet jednocześnie ze sobą. Timery interfejsu działają w wątku interfejsu, dlatego mogą bezpośrednio zmieniać kontrolki i dlatego wolna obsługa zamraża okno.
System.Threading.Timer
Konstruktor przyjmuje callback, obiekt stanu, opóźnienie przed pierwszym tyknięciem i okres. Timer startuje od razu.
Przykładowy wynik:
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
Ważne są tu dwa szczegóły. Main musi czekać (done.WaitOne()); program konsolowy kończy się, gdy Main zwraca sterowanie, a callbacki timera działają w wątkach tła, które nie utrzymują procesu przy życiu. Poza tym timer żyje w polu statycznym: System.Threading.Timer, do którego nic się nie odwołuje, może zostać usunięty przez odśmiecacz, a wtedy po cichu przestaje działać. Najczęściej zdarza się to, gdy timer jest tworzony jako zmienna lokalna w metodzie, która się kończy.
Change(dueTime, period) zmienia harmonogram timera; Timeout.Infinite dla obu wartości go wstrzymuje, a nowa para uruchamia ponownie. Dispose() zatrzymuje go na dobre.
System.Timers.Timer
System.Timers.Timer opakowuje ten sam mechanizm w API oparte na zdarzeniach: ustaw Interval, zasubskrybuj Elapsed, a potem Start() i Stop().
Przykładowy wynik:
autosave #1
autosave #2
autosave #3
autosave stopped
AutoReset = false tworzy timer jednorazowy: uruchamia się raz i zatrzymuje, a ponowne wywołanie Start() planuje kolejne pojedyncze tyknięcie. Enabled = true i false to to samo co Start() i Stop(). Pełna nazwa System.Timers.Timer jest tu zapisana w całości, bo System.Threading też ma Timer; przy zaimportowaniu obu przestrzeni nazw samo Timer jest niejednoznaczne i się nie kompiluje.
System.Timers.Timer ma też właściwość SynchronizingObject, która w WinForms przenosi Elapsed do wątku interfejsu. W praktyce prostszy jest do tego własny timer frameworka interfejsu.
Callbacki nakładają się i działają w innych wątkach
Obie powyższe klasy uruchamiają się w puli wątków zgodnie z harmonogramem, niezależnie od tego, czy poprzedni callback się zakończył. Jeśli callback trwa 3 sekundy, a okres wynosi 1 sekundę, trzy callbacki działają naraz. To rodzi dwa obowiązki:
- Wszystko, czego dotyka callback, musi być bezpieczne wątkowo. Do liczników używaj
Interlocked(tak jak przykłady), a do czegoś większego lock. - Chroń się przed nakładaniem, gdy ma to znaczenie. Pomiń tyknięcie, gdy poprzednie jeszcze trwa, albo użyj wzorca, który nie może się nakładać.
Ochrona przez pomijanie z 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);
}
}
Drugą pułapką są wyjątki. Wyjątek, który wydostanie się z callbacku System.Threading.Timer, wyłącza proces, a System.Timers.Timer połyka wyjątki z obsługi Elapsed, więc błąd przechodzi niezauważony. Opakuj ciało callbacku timera w try/catch i zapisuj błędy w logu.
PeriodicTimer i pętle asynchroniczne
W kodzie asynchronicznym najczytelniejszym timerem jest pętla, która czeka między iteracjami: następne czekanie zaczyna się dopiero po zakończeniu pracy, więc tyknięcia nigdy się nie nakładają, a wyjątki pojawiają się tam, gdzie możesz je złapać. .NET 6 dodał do tego właśnie PeriodicTimer:
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
WaitForNextTickAsync zwraca false po zwolnieniu timera, co kończy pętlę, i rzuca OperationCanceledException, gdy token zostanie anulowany. W przeciwieństwie do pętli wokół Task.Delay PeriodicTimer utrzymuje stały rytm: jeśli praca trwa 2 sekundy, następne tyknięcie i tak przychodzi 30 sekund po poprzednim tyknięciu, a nie po 32.
W każdej wersji ten sam kształt z Task.Delay działa, gdy przesunięcie w czasie nie ma znaczenia:
Przykładowy wynik:
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
Anulowanie zastępuje Stop(): przekaż token do pętli i anuluj go z zewnątrz. Więcej o tokenach anulowania znajdziesz na stronie o taskach.
Mierzenie upływu czasu przez Stopwatch
Timer planuje kod; Stopwatch mierzy, ile kod trwał. Odczytuje zegar o wysokiej rozdzielczości, który porusza się tylko do przodu, co czyni go właściwym narzędziem do pomiaru czasu:
Przykładowy wynik:
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
Sterują nim Start, Stop, Reset i Restart; Elapsed to TimeSpan, a ElapsedMilliseconds i ElapsedTicks dają surowe liczby. Unikaj mierzenia czasu przez DateTime.Now: jego rozdzielczość może wynosić nawet od 10 do 15 ms i skacze przy korekcie zegara systemowego. Do poważnych benchmarków, w których rozgrzewanie JIT i odśmiecanie zniekształcają pojedyncze uruchomienie, użyj biblioteki BenchmarkDotNet.
Dokładność timerów
Interwał timera to minimum, a nie obietnica. W Windows domyślna rozdzielczość zegara systemowego to około 15,6 ms, więc interwał 10 ms zwykle uruchamia się co 15 lub 16 ms, a na obciążonej maszynie callback z puli wątków może wystartować jeszcze później. Timery pasują do zadań typu "mniej więcej co N sekund". Do precyzyjnego odmierzania klatek albo multimediów używaj API do tego przeznaczonych.
Częste błędy
System.Threading.Timerprzechowywany tylko w zmiennej lokalnej. Może zostać usunięty przez odśmiecacz i przestać działać; trzymaj go w polu.- Pozwolenie, żeby
Mainsię zakończył. Proces się kończy i zabiera timer ze sobą. - Niejednoznaczny
Timer. Gdy zaimportowane są iSystem.Threading, iSystem.Timers, pisz pełną nazwę. - Zakładanie, że tyknięcia nigdy się nie nakładają. Timery z puli wątków nakładają się, gdy praca jest wolniejsza niż interwał.
- Zmienianie kontrolek interfejsu z timera z puli wątków. Użyj timera frameworka interfejsu albo przenieś wywołanie z powrotem do wątku interfejsu.
- Nieobsłużone wyjątki w callbackach. Wyłączają proces (
Threading.Timer) albo znikają (Timers.Timer); łap je i zapisuj w logu. - Zapominanie o
Dispose. Timery trzymają zasoby systemowe i działają, dopóki nie zostaną zwolnione.
Najczęściej zadawane pytania
Którego timera używać w C#?
W kodzie asynchronicznym na .NET 6 lub nowszym: PeriodicTimer z await timer.WaitForNextTickAsync() w pętli. Do callbacku w puli wątków: System.Threading.Timer. Do timera opartego na zdarzeniach, ze Start, Stop i zdarzeniem Elapsed: System.Timers.Timer. W WinForms albo WPF użyj timera frameworka interfejsu, żeby obsługa działała w wątku interfejsu.
Jak uruchamiać kod co kilka sekund w C#?
Utwórz timer z interwałem, na przykład new System.Threading.Timer(_ => Refresh(), null, 0, 5000), żeby wywołać Refresh od razu i potem co 5 sekund, i zachowaj do niego referencję. W kodzie asynchronicznym zrób pętlę z PeriodicTimer (.NET 6+) albo await Task.Delay(5000); wersja z pętlą nigdy nie wykonuje dwóch tyknięć naraz.
Jak zatrzymać timer w C#?
W System.Timers.Timer wywołaj Stop() (albo ustaw Enabled = false), a Dispose(), gdy timer nie jest już potrzebny. W System.Threading.Timer wywołaj Change(Timeout.Infinite, Timeout.Infinite), żeby go wstrzymać, albo Dispose(), żeby zatrzymać go na dobre. Callback, który już się rozpoczął, może się jeszcze zakończyć po zatrzymaniu timera.
Dlaczego mój System.Threading.Timer przestaje działać?
Nic nie odwołuje się do timera, więc odśmiecacz go usunął i callbacki ustały. Dzieje się tak, gdy timer jest tworzony jako zmienna lokalna w metodzie, która się kończy. Przechowuj go w polu tak długo, jak ma działać.
Jak zmierzyć upływ czasu w C#?
Użyj System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop();, a potem odczytaj sw.ElapsedMilliseconds albo sw.Elapsed. Korzysta z monotonicznego zegara o wysokiej rozdzielczości, w przeciwieństwie do odejmowania dwóch wartości DateTime.Now, które ma gorszą rozdzielczość i skacze przy korekcie zegara systemowego.