Zadania okresowe i timery
Planuj cykliczne zadania za pomocą PeriodicTimer i System.Threading.Timer oraz poprawnie obsługuj anulowanie.
Zadania okresowe i timery to bezpłatna lekcja C# Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej C# Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs C# Academy zawiera 4 lekcji w sumie.
Planowanie cyklicznej pracy
Wiele usług działających w tle musi uruchamiać się zgodnie z ustalonym harmonogramem — co 5 minut, co godzinę lub codziennie. .NET udostępnia kilka mechanizmów: PeriodicTimer (nowoczesny), System.Threading.Timer (klasyczny) oraz Quartz.NET (oparty na wyrażeniach cron).
PeriodicTimer: nowoczesny wybór
PeriodicTimer (wprowadzony w .NET 6) został zaprojektowany do użycia w metodach async. Uruchamia się dokładnie zgodnie z harmonogramem, bez callbacków, i płynnie integruje się z anulowaniem.
public class MetricsCollectorService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
{
// Runs every 60 seconds
await CollectMetricsAsync(ct);
}
// Loop exits cleanly when ct is cancelled
}
}PeriodicTimer a pętla Task.Delay
PeriodicTimer jest lepszy od pętli z Task.Delay: uruchamia się w stałych odstępach liczonych od czasu rozpoczęcia, a nie od czasu rozpoczęcia powiększonego o czas przetwarzania, dzięki czemu po wielu iteracjach nie występuje dryf harmonogramu.
// Task.Delay loop — drifts! (interval = delay + work time)
while (!ct.IsCancellationRequested)
{
await DoWorkAsync(ct); // takes 2 seconds
await Task.Delay(5000, ct); // actual period: 7 seconds
}
// PeriodicTimer — no drift
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
await DoWorkAsync(ct); // period is still 5 seconds from start
}System.Threading.Timer
System.Threading.Timer to klasyczny timer puli wątków korzystający z callbacku. Obsługuje zarówno tryb okresowy, jak i jednorazowy. Należy zachować ostrożność: callback działa w wątku puli wątków bez obsługi async.
public class LegacyTimerService : BackgroundService, IDisposable
{
private Timer? _timer;
protected override Task ExecuteAsync(CancellationToken ct)
{
// Fire immediately then every 10 seconds
_timer = new Timer(DoWork, null,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
// Runs on thread pool — avoid async void here
Console.WriteLine($"Timer fired: {DateTime.UtcNow}");
}
public override void Dispose() { _timer?.Dispose(); base.Dispose(); }
}Jednorazowe wykonanie z opóźnieniem
Do jednorazowego wykonania z opóźnieniem użyj PeriodicTimer z pojedynczym tyknięciem albo połącz CancellationTokenSource.CancelAfter z Task.Delay.
// Run once after a delay
public async Task ScheduleOnceAsync(TimeSpan delay, Func<Task> action, CancellationToken ct)
{
await Task.Delay(delay, ct);
await action();
}
// Or with CancellationToken:
var cts = new CancellationTokenSource(TimeSpan.FromHours(1));
try
{
await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException)
{
await RunScheduledJobAsync();
}PeriodicTimer z obsługą błędów
Zawsze umieszczaj pracę wykonywaną przy każdym tyknięciu w bloku try/catch, aby pojedyncze niepowodzenie nie zatrzymało kolejnych tyknięć timera.
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
await SyncInventoryAsync(ct);
}
catch (Exception ex)
{
// Log but don't crash — next tick will retry
_logger.LogError(ex, "Inventory sync failed");
}
}
}Wyrównywanie do granic czasu
Aby uruchamiać zadania o konkretnych godzinach (np. co godzinę, równo o pełnej godzinie), przed uruchomieniem timera oblicz początkowe opóźnienie do najbliższej granicy.
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Wait until the next full hour
var now = DateTime.UtcNow;
var nextHour = now.Date.AddHours(now.Hour + 1);
var initialDelay = nextHour - now;
await Task.Delay(initialDelay, ct);
// Then tick every hour exactly
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
await RunJobAsync(ct); // run at the first tick
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}Ochrona przed równoczesnymi tyknięciami
Jeśli wykonanie pracy może trwać dłużej niż jeden okres timera, zabezpiecz się przed równoczesnym wykonaniem za pomocą SemaphoreSlim albo pomiń tyknięcie, jeśli poprzednia praca nadal trwa.
private readonly SemaphoreSlim _lock = new(1, 1);
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(ct))
{
// Skip if previous tick is still running
if (!await _lock.WaitAsync(0, ct))
{
_logger.LogWarning("Skipping tick — previous still running");
continue;
}
try { await DoWorkAsync(ct); }
finally { _lock.Release(); }
}
}Jitter w usługach rozproszonych
Gdy wiele instancji usługi uruchamia się jednocześnie, dodanie losowego jittera do początkowego opóźnienia zapobiega problemom typu „lawina żądań” dotyczących współdzielonych zasobów.
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Add jitter: 0-5 seconds random delay on startup
var jitter = TimeSpan.FromSeconds(Random.Shared.NextDouble() * 5);
await Task.Delay(jitter, ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}Praktyczny przykład: usługa odświeżania pamięci podręcznej
Usługa okresowo odświeżająca rozproszoną pamięć podręczną co 10 minut, z użyciem jittera i odzyskiwaniem po błędach.
public class CacheRefreshService : BackgroundService
{
private readonly IServiceScopeFactory _factory;
private readonly ILogger<CacheRefreshService> _log;
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Spread startups across instances
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 30)), ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
using var scope = _factory.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
await cache.RefreshAllAsync(ct);
_log.LogInformation("Cache refreshed at {Time}", DateTime.UtcNow);
}
catch (Exception ex) { _log.LogError(ex, "Cache refresh failed"); }
}
}
}Szybkie sprawdzenie
Dlaczego PeriodicTimer jest preferowany zamiast pętli Task.Delay w przypadku zadań cyklicznych?
Podsumowanie: zadania okresowe i timery
Najważniejsze informacje:
PeriodicTimer: obsługa async i anulowania, brak dryfu — nowoczesny wybórSystem.Threading.Timer: oparty na callbacku, dobry do integracji ze starszym kodem- Pracę wykonywaną przy każdym tyknięciu umieszczaj w bloku try/catch, aby błędy nie zatrzymywały timera
- Dodaj jitter dla rozproszonych instancji, aby uniknąć problemu „lawiny żądań”
- Użyj
SemaphoreSlim, aby pominąć tyknięcie, jeśli poprzednie nadal trwa - Oblicz początkowe opóźnienie, aby wyrównać tyknięcia do granic czasu
Ucz się C# dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 93
- Lekcje
- 346
Często zadawane pytania
Czy lekcja „Zadania okresowe i timery” jest bezpłatna?
Tak — pełny tekst „Zadania okresowe i timery” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu C# Academy, przejdź na CoddyKit PRO. Kurs C# Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Zadania okresowe i timery”?
Planuj cykliczne zadania za pomocą PeriodicTimer i System.Threading.Timer oraz poprawnie obsługuj anulowanie. Ćwiczysz C# Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć C# Academy?
Nie wymagamy żadnego doświadczenia. C# Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Zadania okresowe i timery”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji C# Academy?
Tak. Każda lekcja C# Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- IHostedService i BackgroundService
- Projekty Worker Service
- Zadania okresowe i timery
- Zaplanowane zadania Quartz.NET