CodeGym /Corsi /C# SELF /Eccezioni nelle task "fire and forget"

Eccezioni nelle task "fire and forget"

C# SELF
Livello 61, Lezione 0
Disponibile

1. Cos'è "fire and forget"?

In programmazione il termine fire and forget significa avviare un'operazione senza attendere il suo completamento. Nel mondo C# e .NET questo si fa spesso con Task che vengono lanciati ma non await-ati, non si mantiene il riferimento e praticamente vengono dimenticati.

// Il bottone avvia una task in background, ma da nessuna parte viene await-ata.
button.Click += (s, e) =>
{
    Task.Run(() => DolgaiaOperaciia());
};

Suona comodo: "lascia che lavori in background e io faccio altro". Ma con questo approccio, se dentro la task si verifica un'eccezione, nessuno lo saprà in tempo — verrà silenziosamente persa.

2. Come funziona la gestione delle eccezioni in un Task

Classico: await e gestione degli errori

Il modo standard per lavorare con le task asincrone è tramite await. Se nella task accade un errore, verrà rilanciato nel punto di attesa:

try
{
    await SomeOperationAsync(); // se qui dentro c'è un Exception, andrà nel catch
}
catch(Exception ex)
{
    Console.WriteLine("Ops! Nella task è successo un errore: " + ex.Message);
}

Quindi quando aspetti il completamento della task, non perdi l'eccezione.

Ma le task "fire and forget" non vengono attese!

public void ZapustitBezOzhidaniia()
{
    // La task è per conto suo. Nessuno la aspetta...
    Task.Run(() => {
        // Da qualche parte dentro succede un disastro:
        throw new InvalidOperationException("Oi, tut vse propalo!");
    });
    // Il metodo è terminato, la task lavora silenziosamente in background.
}

Se dentro questa task si verifica un'eccezione, essa non verrà lanciata nel thread principale. L'app continuerà a funzionare come se nulla fosse successo.

Importante

In .NET una task con un'eccezione non gestita passa nello stato Faulted. Ma se non la await-i (await, .Result, .Wait() ecc.), nessuno leggerà l'eccezione e non si manifesterà nel codice chiamante.

Cosa succede "sotto il cofano"?

Per le task che nessuno aspetta, rimane un'unica possibilità per essere notate — l'evento TaskScheduler.UnobservedTaskException. Questo scatta quando il garbage collector (GC) rileva una task con eccezione non osservata. Ma ciò non avviene subito né nel punto in cui te lo aspetti — non si può fare affidamento su questo.

3. Dimostrazione: errore fire-and-forget

// Esempio: lanciamo una task fire-and-forget direttamente da Main
using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        FireAndForgetExample();

        Console.WriteLine("Il thread principale continua a lavorare...");
        // Diamo tempo alla task di finire
        Task.Delay(2000).Wait();
    }

    static void FireAndForgetExample()
    {
        Task.Run(() =>
        {
            Console.WriteLine("La task fire-and-forget è iniziata!");
            Task.Delay(500).Wait();
            throw new InvalidOperationException("Errore dentro la task fire-and-forget!");
        });
    }
}

Se esegui questo codice, ... non succederà nulla di apparente. L'errore accade, ma il programma non lo saprà. A volte si vede un warning nella Output Window dell'IDE, ma per l'utente — zero informazioni.

Perché è pericoloso nei progetti reali?

  • Bug complessi difficili da riprodurre ("a volte non funziona — non si capisce perché").
  • Perdita silenziosa di dati o logica (es. mail non inviata al cliente).
  • In produzione — mancanza di segnali sui problemi se non è impostato il logging.

4. Modi corretti per gestire errori in fire-and-forget

Logging e gestione degli errori dentro la stessa task

Il livello minimo sicuro è catturare le eccezioni dentro la stessa task fire-and-forget:

Task.Run(() =>
{
    try
    {
        // Il tuo codice lungo/pericoloso
        throw new InvalidOperationException("Qualcosa è andato storto!");
    }
    catch (Exception ex)
    {
        // Logghiamo l'errore o informiamo l'utente
        Console.WriteLine("Fire-and-forget: eccezione catturata: " + ex.Message);
        // Si può scrivere su file di log, inviare ad un sistema di alert ecc.
    }
});

Metodi async void (e perché non è una buona idea)

async void DangerousFireAndForget()
{
    // Qualcosa di pericoloso
    throw new Exception("Bdysh!");
}

I metodi async void sono di fatto fire-and-forget: non è possibile aspettarli, non restituiscono Task. Le eccezioni da essi vanno al gestore globale dell'app (es. AppDomain.UnhandledException) e spesso portano al crash del processo. Usa async void solo per gli event handler — e comunque con cautela.

Uso di helper per gestire errori

Comodo estrarre l'avvio sicuro di fire-and-forget in un wrapper:

// Metodo generico per lanciare in sicurezza un fire-and-forget
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
    Task.Run(async () =>
    {
        try
        {
            await taskFactory();
        }
        catch (Exception ex)
        {
            // Logghiamo l'eccezione
            Console.WriteLine("Fire-and-forget (safe): " + ex);
            // Si può aggiungere l'invio a un sistema di monitoraggio!
        }
    });
}

// Utilizzo:
RunSafeFireAndForget(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException("Dentro il fire-and-forget!");
});

Esempio "reale": invio email

// Bottone per invio mail:
private void buttonSend_Click(object sender, EventArgs e)
{
    Task.Run(() => SendEmail());
}

// Metodo di invio:
private void SendEmail()
{
    try
    {
        // Qui potrebbe esserci l'invio reale
        throw new Exception("SMTP-server nedostupen!");
    }
    catch (Exception ex)
    {
        // Logging
        File.AppendAllText("errors.log", $"Oshibka otpravki: {ex.Message}\n");
    }
}

5. E che dire di UnobservedTaskException?

Come ultima risorsa, .NET fornisce l'evento TaskScheduler.UnobservedTaskException. Viene chiamato se una task è terminata con errore, nessuno l'ha await-ata e l'oggetto task è stato raccolto dal GC. Non fare affidamento su questo — è un meccanismo di "ultima chance".

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Console.WriteLine("Globale UnobservedTaskException: " + e.Exception);
    e.SetObserved(); // Non dimenticare di chiamarlo, altrimenti l'app può terminare in modo anomalo!
};

Per approfondire: TaskScheduler.UnobservedTaskException.

6. Dettagli utili

Confronto schematico degli approcci

Metodo Eccezioni gestite? Dove catturare gli errori Rischio di "perdere" l'errore
await
Sì Nel codice chiamante Basso
Fire-and-forget senza try/catch No In nessun posto Molto alto
Fire-and-forget con try/catch Sì Dentro la task stessa Basso (se fai logging)
async void-metodo No (va al globale) Gestore globale Alto

Come progettare correttamente fire-and-forget

  • Se il risultato o lo stato della task è critico — non usare fire-and-forget. Usa await o conserva il Task per aspettarlo successivamente.
  • Fire-and-forget è giustificato solo per task di background davvero non critiche (es. invio di telemetria).
  • Avvolgi sempre fire-and-forget in un metodo proprio e cattura/logga le eccezioni.
  • Per scenari di background complessi usa code e worker: Hangfire, Quartz.NET.

Uso pratico e colloqui

Nei colloqui spesso chiedono: "Cosa succede se in una task fire-and-forget si verifica un'eccezione?" o "Perché non usare everywhere async void?" La risposta corretta: sei tu responsabile degli errori nelle task di background — o le catturi, le logghi e le analizzi, o ti ritrovi bug fantasma.

Confronto "fire-and-forget" vs await

Scenario Affidabilità nella gestione errori Applicabilità
Await normale Eccellente Dovunque sia necessario il risultato o sia importante il successo/fallimento
Fire-and-forget Pessima (se non gestita manualmente) Solo per task davvero di background e non critiche
Fire-and-forget con try/catch Buona (se logghi) Task di background dove non serve il risultato ma è importante sapere dei fallimenti

Nella prossima lezione discuteremo la gestione degli errori in attività parallele che restituiscono più risultati. Intanto ricordate: se avete "sparato" qualcosa, assicuratevi che sia arrivato a destinazione!

7. Errori tipici quando si lavora con fire-and-forget

Errore №1: Ignorare le eccezioni nei fire-and-forget.
I principianti sperano che le eccezioni "emergano da qualche parte". Senza try-catch e logging si perdono, portando a bug non rilevati.

Errore №2: Usare async void al di fuori degli event handler.
Questi metodi buttano le eccezioni al gestore globale (es. AppDomain.UnhandledException), il che può far terminare l'app in modo anomalo.

Errore №3: Gestire eccessivamente le eccezioni.
Catturare ogni eccezione dentro la task può nascondere problemi che sarebbe meglio gestire nel codice chiamante, rendendo il debug più difficile.

Errore №4: Trascurare il logging.
Senza log delle eccezioni nelle task fire-and-forget è impossibile sapere dei fallimenti, specialmente in produzione.

2
Compito
C# SELF, livello 61, lezione 0
Bloccato
Gestore globale `UnobservedTaskException`
Gestore globale `UnobservedTaskException`
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION