C# Academy · Lekcja

Zadania okresowe i timery

Planuj cykliczne zadania za pomocą PeriodicTimer i System.Threading.Timer oraz poprawnie obsługuj anulowanie.

Lekcja 3 z 412 kroki

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ór
  • System.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
Bezpłatny start

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

  1. IHostedService i BackgroundService
  2. Projekty Worker Service
  3. Zadania okresowe i timery
  4. Zaplanowane zadania Quartz.NET
← Powrót do C# Academy