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 modernaSystem.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
SemaphoreSlimper 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
- IHostedService e BackgroundService
- Progetti Worker Service
- Attività periodiche e timer
- Processi pianificati con Quartz.NET