0Pricing
C# Academy · Lezione

Attività periodiche e timer

Pianifichi attività ricorrenti con PeriodicTimer e System.Threading.Timer e gestisca l'annullamento in modo corretto.

Attività periodiche e timer è una lezione C# Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento C# Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso C# Academy include 4 lezioni in totale.

Pianificazione delle attività ricorrenti

Molti servizi in background devono essere eseguiti secondo una pianificazione fissa: ogni 5 minuti, ogni ora o ogni giorno. .NET fornisce diversi meccanismi: PeriodicTimer (moderno), System.Threading.Timer (classico) e Quartz.NET (basato su cron).

PeriodicTimer: la scelta moderna

PeriodicTimer (introdotto in .NET 6) è progettato per essere utilizzato nei metodi async. Si attiva esattamente secondo la pianificazione, senza callback, e si integra in modo lineare con l'annullamento.

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 o ciclo Task.Delay

PeriodicTimer è migliore di un ciclo con Task.Delay: si attiva a intervalli fissi a partire dall'ora di inizio, non dall'ora di inizio più il tempo di elaborazione, evitando derive dopo molte iterazioni.

// 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 è un timer classico del pool di thread che utilizza una callback. Supporta sia la modalità periodica sia quella a esecuzione singola. Presti attenzione: la callback viene eseguita su un thread del pool senza supporto asincrono.

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(); }
}

Esecuzione ritardata una tantum

Utilizzi PeriodicTimer con un singolo tick oppure combini CancellationTokenSource.CancelAfter con Task.Delay per un'esecuzione ritardata una tantum.

// 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 con gestione degli errori

Racchiuda sempre il lavoro di ogni tick in try/catch, in modo che un singolo errore non impedisca al timer di attivarsi nuovamente nel ciclo successivo.

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");
        }
    }
}

Allineamento ai confini dell'orologio

Per eseguire un'attività a orari specifici (ad esempio ogni ora allo scoccare dell'ora), calcoli il ritardo iniziale fino al confine temporale successivo prima di avviare il timer.

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);
}

Protezione dalle attivazioni concorrenti

Se il lavoro può richiedere più tempo di un periodo del timer, protegga l'esecuzione concorrente con un SemaphoreSlim oppure ignori il tick se l'esecuzione è ancora in corso.

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 per i servizi distribuiti

Quando più istanze di un servizio vengono avviate contemporaneamente, aggiungere un jitter casuale al ritardo iniziale evita i problemi di sovraccarico a cascata sulle risorse condivise.

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);
}

Caso reale: servizio di aggiornamento della cache

Un servizio periodico che aggiorna una cache distribuita ogni 10 minuti, con jitter e recupero dagli errori.

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"); }
        }
    }
}

Verifica rapida

Perché PeriodicTimer è preferibile a un ciclo con Task.Delay per le attività ricorrenti?

Riepilogo: attività periodiche e timer

Punti chiave:

  • PeriodicTimer: asincrono, consapevole dell'annullamento e senza derive: la scelta moderna
  • System.Threading.Timer: basato su callback, utile per l'integrazione con codice legacy
  • Racchiuda il lavoro di ogni tick in try/catch, in modo che gli errori non arrestino il timer
  • Aggiunga un jitter per le istanze distribuite, così da evitare il sovraccarico a cascata
  • Utilizzi SemaphoreSlim per ignorare un tick se quello precedente è ancora in esecuzione
  • Calcoli il ritardo iniziale per allineare i tick ai confini dell'orologio

Domande Frequenti

La lezione «Attività periodiche e timer» è gratuita?

Sì — il testo completo di «Attività periodiche e timer» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso C# Academy, passa a CoddyKit PRO. Il corso C# Academy include 4 lezioni in totale.

Cosa imparerò in «Attività periodiche e timer»?

Pianifichi attività ricorrenti con PeriodicTimer e System.Threading.Timer e gestisca l'annullamento in modo corretto. Eserciti C# Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare C# Academy?

Non è richiesta alcuna esperienza precedente. C# Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.

Quanto tempo richiede la lezione «Attività periodiche e timer»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione C# Academy?

Sì. Ogni lezione C# Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. IHostedService e BackgroundService
  2. Progetti Worker Service
  3. Attività periodiche e timer
  4. Processi pianificati con Quartz.NET
← Torna a C# Academy