Menu

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

C# ma kilka klas timerów i każda działa inaczej. Dowiedz się, kiedy używać System.Threading.Timer, System.Timers.Timer, PeriodicTimer albo timera interfejsu, jak je uruchamiać i zatrzymywać, dlaczego callbacki działają w puli wątków i mogą się nakładać, dlaczego do timera musi prowadzić referencja i jak mierzyć upływ czasu przez Stopwatch.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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ć

TimerCallback działa wStylZastosowanie
System.Threading.TimerPuli wątkówDelegat callbackuPraca w tle w usługach i bibliotekach
System.Timers.TimerPuli wątków (domyślnie)Zdarzenie Elapsed, Start/StopKod w stylu komponentów, oparty na zdarzeniach
PeriodicTimer (.NET 6+)Tam, gdzie działa twoja pętla asynchronicznaawait WaitForNextTickAsync()Kod asynchroniczny; tyknięcia nigdy się nie nakładają
System.Windows.Forms.Timer, DispatcherTimer z WPFWątku interfejsuZdarzenie TickAktualizacja 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.Timer przechowywany tylko w zmiennej lokalnej. Może zostać usunięty przez odśmiecacz i przestać działać; trzymaj go w polu.
  • Pozwolenie, żeby Main się zakończył. Proces się kończy i zabiera timer ze sobą.
  • Niejednoznaczny Timer. Gdy zaimportowane są i System.Threading, i System.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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ