1. Was ist "fire and forget"?
In der Programmierung bedeutet der Begriff fire and forget, eine Aufgabe zu starten, ohne auf deren Abschluss zu warten. In der Welt von C# und .NET macht man das meist mit Task-Objekten, die man startet, aber nirgends mit await erwartet, keinen Verweis speichert und sie praktisch vergisst.
// Die Schaltfläche startet eine Hintergrundaufgabe, die nirgends await-ed wird.
button.Click += (s, e) =>
{
Task.Run(() => DolgayaOperatsiya());
};
Klingt verlockend: "lass es im Hintergrund laufen, ich mache weiter". Aber bei diesem Ansatz, wenn in der Aufgabe eine Ausnahme auftritt, weiß niemand rechtzeitig davon — sie geht still verloren.
2. Wie funktioniert die Ausnahmebehandlung in Task
Klassiker: await und Fehlerbehandlung
Der Standardweg mit asynchronen Aufgaben ist über await. Wenn in der Aufgabe ein Fehler passiert, wird er an der Wartestelle geworfen:
try
{
await SomeOperationAsync(); // wenn hier drinnen Exception, landet sie im catch
}
catch(Exception ex)
{
Console.WriteLine("Ups! In der Aufgabe ist ein Fehler aufgetreten: " + ex.Message);
}
Das heißt: Wenn ihr auf die Aufgabe wartet, verpasst ihr die Ausnahme nicht.
Aber "fire and forget"-Aufgaben wartet niemand!
public void ZapustitBezOzhidaniya()
{
// Die Aufgabe läuft für sich. Niemand wartet auf sie...
Task.Run(() => {
// Irgendwo drinnen passiert ein Unglück:
throw new InvalidOperationException("Oh nein, alles ist kaputt!");
});
// Die Methode ist beendet, die Aufgabe läuft still im Hintergrund.
}
Wenn in so einer Aufgabe eine Ausnahme auftritt, wird sie im Hauptthread nicht geworfen. Die Anwendung läuft weiter, als wäre nichts geschehen.
Wichtig
In .NET geht eine Aufgabe mit einer unbehandelten Ausnahme in den Zustand Faulted über. Wenn ihr sie aber nicht erwartet (await, .Result, .Wait() usw.), wird die Ausnahme von niemandem gelesen und äußert sich nicht im aufrufenden Code.
Was passiert wirklich "unter der Haube"?
Für Aufgaben, auf die nicht gewartet wird, bleibt als einzige Chance, bemerkt zu werden, das Ereignis TaskScheduler.UnobservedTaskException. Es wird ausgeführt, wenn der Garbage Collector (GC) eine Aufgabe mit unbehandelter Ausnahme findet. Aber das passiert nicht sofort und nicht an der Stelle, die ihr erwartet — darauf sollte man sich nicht verlassen.
3. Demonstration: Fire-and-forget-Fehler
// Beispiel: wir starten eine fire-and-forget-Aufgabe direkt aus Main
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
FireAndForgetExample();
Console.WriteLine("Der Hauptthread läuft weiter...");
// Geben wir der Aufgabe Zeit, fertig zu werden
Task.Delay(2000).Wait();
}
static void FireAndForgetExample()
{
Task.Run(() =>
{
Console.WriteLine("Fire-and-forget-Aufgabe hat begonnen!");
Task.Delay(500).Wait();
throw new InvalidOperationException("Fehler in der fire-and-forget-Aufgabe!");
});
}
}
Wenn man diesen Code startet, passiert... nichts Besonderes. Der Fehler tritt auf, aber das Programm erfährt nichts davon. Manchmal sieht man eine Warnung im Output Window der IDE, für den Benutzer gibt es jedoch keine Information.
Warum ist das in echten Projekten gefährlich?
- Komplizierte Bugs, die schwer zu reproduzieren sind ("manchmal funktioniert es nicht — warum?").
- Stiller Daten- oder Logikverlust (z. B. eine E-Mail wurde nicht gesendet).
- In Produktion: keine Signale über Probleme, wenn kein Logging eingerichtet ist.
4. Korrekte Wege zur Fehlerbehandlung in fire-and-forget
Logging und Fehlerbehandlung innerhalb der Aufgabe
Das minimale sichere Niveau ist, Ausnahmen direkt innerhalb der fire-and-forget-Aufgabe abzufangen:
Task.Run(() =>
{
try
{
// Euer langer/gefährlicher Code
throw new InvalidOperationException("Etwas ist schiefgelaufen!");
}
catch (Exception ex)
{
// Loggen oder den Benutzer informieren
Console.WriteLine("Fire-and-forget: gefangene Ausnahme: " + ex.Message);
// Man kann in eine Log-Datei schreiben, Monitoring anrufen usw.
}
});
Asynchrone void-Methoden (und warum man das nicht tun sollte)
async void DangerousFireAndForget()
{
// Etwas Gefährliches
throw new Exception("Bumm!");
}
async void-Methoden sind im Grunde fire-and-forget: man kann nicht auf sie warten, sie geben kein Task zurück. Ausnahmen aus ihnen landen im globalen Anwendungs-Handler (z. B. AppDomain.UnhandledException) und führen oft zum Absturz des Prozesses. Verwende async void nur für Event-Handler — und auch da mit Vorsicht.
Verwendung von Hilfsmethoden zur Fehlerbehandlung
Praktisch ist es, den sicheren Start von fire-and-forget in einen Wrapper auszulagern:
// Universelle Methode zum sicheren Starten von fire-and-forget
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
Task.Run(async () =>
{
try
{
await taskFactory();
}
catch (Exception ex)
{
// Loggen der Ausnahme
Console.WriteLine("Fire-and-forget (safe): " + ex);
// Man kann hier auch Monitoring anrufen!
}
});
}
// Verwendung:
RunSafeFireAndForget(async () =>
{
await Task.Delay(1000);
throw new InvalidOperationException("Drinnen in fire-and-forget!");
});
Praxisbeispiel: E-Mail versenden
// Button zum Senden der Mail:
private void buttonSend_Click(object sender, EventArgs e)
{
Task.Run(() => SendEmail());
}
// Versand-Methode:
private void SendEmail()
{
try
{
// Hier könnte der echte Versand stattfinden
throw new Exception("SMTP-Server ist nicht erreichbar!");
}
catch (Exception ex)
{
// Logging
File.AppendAllText("errors.log", $"Fehler beim Versand: {ex.Message}\n");
}
}
5. Und was ist mit UnobservedTaskException?
Als letzte Instanz stellt .NET das Event TaskScheduler.UnobservedTaskException zur Verfügung. Es wird aufgerufen, wenn eine Task mit Fehler beendet wurde, niemand auf sie gewartet hat und das Task-Objekt vom GC gesammelt wurde. Darauf sollte man sich nicht verlassen — es ist ein "letzter Ausweg"-Mechanismus.
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine("Globaler UnobservedTaskException: " + e.Exception);
e.SetObserved(); // Nicht vergessen aufzurufen, sonst kann die Anwendung abstürzen!
};
Mehr dazu: TaskScheduler.UnobservedTaskException.
6. Nützliche Feinheiten
Schematischer Vergleich der Ansätze
| Methode | Ausnahmen behandelt? | Wo Fehler fangen | Risiko, den Fehler zu "verlieren" |
|---|---|---|---|
|
Ja | Im aufrufenden Code | Niedrig |
| Fire-and-forget ohne try/catch | Nein | Nirgends | Sehr hoch |
| Fire-and-forget mit try/catch | Ja | Innerhalb der Aufgabe selbst | Niedrig (wenn geloggt) |
| async void-Methode | Nein (geht an global) | Globaler Handler | Hoch |
Wie man fire-and-forget richtig gestaltet
- Wenn das Ergebnis oder der Zustand der Aufgabe kritisch ist — macht kein fire-and-forget. Verwendet await oder speichert die Task zum späteren Warten.
- Fire-and-forget ist nur für wirklich unkritische Hintergrundaufgaben gerechtfertigt (z. B. Telemetrie senden).
- Packt fire-and-forget immer in eine eigene Methode und fangt/loggt Ausnahmen.
- Für komplexe Hintergrundszenarien nutzt Queues und Worker: Hangfire, Quartz.NET.
Praktische Anwendung und Vorstellungsgespräche
In Interviews wird oft gefragt: "Was passiert, wenn in einer fire-and-forget-Aufgabe eine Ausnahme auftritt?" oder "Warum kann man nicht überall async void verwenden?" Die richtige Antwort: Ihr seid allein verantwortlich für das Schicksal von Fehlern in Hintergrundaufgaben — entweder fangt, loggt und analysiert ihr sie, oder ihr bekommt Geisterbugs.
Gegenüberstellung: "fire-and-forget" vs await
| Szenario | Zuverlässigkeit der Fehlerbehandlung | Anwendbarkeit |
|---|---|---|
| Normales await | Ausgezeichnet | Überall, wo Ergebnis oder Erfolg/Misserfolg wichtig ist |
| Fire-and-forget | Schlecht (wenn nicht manuell behandelt) | Nur für wirklich Hintergrund- und unwichtige Aufgaben |
| Fire-and-forget mit try/catch | Gut (wenn geloggt) | Hintergrundaufgaben, bei denen kein Ergebnis benötigt wird, aber Fehler wichtig sind |
In der nächsten Vorlesung besprechen wir Fehlerbehandlung in parallelen Tasks, die mehrere Ergebnisse zurückliefern. Bis dahin: Wenn ihr irgendwo "abgeschossen" habt, prüft nach, ob das Ziel erreicht wurde!
7. Typische Fehler beim Arbeiten mit fire-and-forget-Aufgaben
Fehler Nr.1: Ignorieren von Ausnahmen in fire-and-forget.
Einsteiger hoffen, dass Ausnahmen "irgendwo hochkommen". Ohne try-catch und Logging gehen sie verloren und führen zu unentdeckten Bugs.
Fehler Nr.2: Verwendung von async void außerhalb von Event-Handlern.
Solche Methoden werfen Ausnahmen in den globalen Handler (z. B. AppDomain.UnhandledException), was die Anwendung abstürzen lassen kann.
Fehler Nr.3: Übermäßiges Abfangen von Ausnahmen.
Das Fangen aller Ausnahmen innerhalb der Aufgabe kann Probleme verbergen, die besser im aufrufenden Code behandelt würden und erschwert das Debugging.
Fehler Nr.4: Vernachlässigung des Loggings.
Ohne Logging ist es unmöglich, Fehler in fire-and-forget-Aufgaben herauszufinden, besonders in Produktion.
GO TO FULL VERSION