Asynchrone Methoden lebendig halten

Feuer-und-Vergessen-Arbeit ist einfach zu starten und leicht zu verlieren. Wenn Sie einen asynchronen Vorgang starten und den zurückgegebenen Task ignorieren, verlieren Sie die Sichtbarkeit hinsichtlich des Abschlusses, der Stornierung und der Fehlschläge.

Die meisten Lebensdauerfehler in asynchronem Code sind Besitzerfehler, nicht Compilerfehler. Der async Zustandsautomat und seine Task bleiben funktionsfähig, solange Arbeit noch durch Continuations erreichbar ist. Probleme treten auf, wenn Ihre App diese Arbeit nicht mehr nachverfolgt.

Warum "Feuer und Vergessen" zu Lebensdauerfehlern führt

Wenn Sie mit der Hintergrundarbeit beginnen, ohne sie zu verfolgen, erstellen Sie drei Risiken:

  • Der Vorgang kann fehlschlagen, und niemand beobachtet die Ausnahme.
  • Der Prozess oder Host kann heruntergefahren werden, bevor der Vorgang abgeschlossen ist.
  • Der Vorgang kann das Objekt oder den Bereich überleben, die ihn steuern sollten.

Verwenden Sie "Feuer und Vergessen", wenn die Arbeit wirklich optional ist und Fehler akzeptabel sind.

Explizites Nachverfolgen von Hintergrundaufgaben

In diesem Beispiel wird eine benutzerdefinierte Hilfsklasse definiert BackgroundTaskTracker, die ein threadsicheres Wörterbuch von In-Flight-Aufgaben enthält. Wenn Sie aufrufen Track, registriert es eine ContinueWith Fortsetzung für die Aufgabe, die die Aufgabe aus dem Wörterbuch entfernt, wenn sie abgeschlossen ist, und protokolliert einen Fehler. Wenn Sie DrainAsync aufrufen, wird Task.WhenAll für jede Aufgabe im Wörterbuch aufgerufen und die resultierende Aufgabe zurückgegeben.

public sealed class BackgroundTaskTracker
{
    private readonly ConcurrentDictionary<int, Task> _inFlight = new();

    public void Track(Task operationTask, string name)
    {
        int id = operationTask.Id;
        _inFlight[id] = operationTask;

        _ = operationTask.ContinueWith(completedTask =>
        {
            _inFlight.TryRemove(id, out _);

            if (completedTask.IsFaulted)
            {
                Console.WriteLine($"{name} failed: {completedTask.Exception?.GetBaseException().Message}");
            }
        }, TaskScheduler.Default);
    }

    public Task DrainAsync()
    {
        Task[] snapshot = _inFlight.Values.ToArray();
        return snapshot.Length == 0 ? Task.CompletedTask : Task.WhenAll(snapshot);
    }
}
Public NotInheritable Class BackgroundTaskTracker
    Private ReadOnly _inFlight As New ConcurrentDictionary(Of Integer, Task)()

    Public Sub Track(operationTask As Task, name As String)
        Dim id As Integer = operationTask.Id
        _inFlight(id) = operationTask

        Dim continuationTask As Task = operationTask.ContinueWith(Sub(completedTask)
                                                                      Dim removedTask As Task = Nothing
                                                                      _inFlight.TryRemove(id, removedTask)

                                                                      If completedTask.IsFaulted Then
                                                                          Console.WriteLine($"{name} failed: {completedTask.Exception.GetBaseException().Message}")
                                                                      End If
                                                                  End Sub,
                                                                  TaskScheduler.Default)
    End Sub

    Public Function DrainAsync() As Task
        Dim snapshot As Task() = _inFlight.Values.ToArray()

        If snapshot.Length = 0 Then
            Return Task.CompletedTask
        End If

        Return Task.WhenAll(snapshot)
    End Function
End Class

Im folgenden Beispiel wird BackgroundTaskTracker verwendet, um einen Hintergrundvorgang zu starten, zu beobachten und abzuschließen.

public static class FireAndForgetFix
{
    public static async Task RunAsync(BackgroundTaskTracker tracker)
    {
        Task backgroundTask = Task.Run(async () =>
        {
            await Task.Delay(100);
            throw new InvalidOperationException("Background operation failed.");
        });

        tracker.Track(backgroundTask, "Cache refresh");

        try
        {
            await tracker.DrainAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Drain observed failure: {ex.GetBaseException().Message}");
        }
    }
}
Public Module FireAndForgetFix
    Public Async Function RunAsync(tracker As BackgroundTaskTracker) As Task
        Dim backgroundTask As Task = Task.Run(Async Function()
                                                  Await Task.Delay(100)
                                                  Throw New InvalidOperationException("Background operation failed.")
                                              End Function)

        tracker.Track(backgroundTask, "Cache refresh")

        Try
            Await tracker.DrainAsync()
        Catch ex As Exception
            Console.WriteLine($"Drain observed failure: {ex.GetBaseException().Message}")
        End Try
    End Function
End Module

Falls DrainAsync nur auf die von Ihnen gestartete Aufgabe wartet, warum dann nicht direkt await backgroundTask und den Tracker vollständig überspringen? Bei einem einzelnen Vorgang in einer einzigen Methode könnten Sie es tun. Der Tracker wird nützlich, wenn Aufgaben von vielen verschiedenen Stellen innerhalb der Lebensdauer einer Komponente gestartet werden. Jede Anruf übergibt seine Aufgabe an den freigegebenen Tracker, und ein einzelner DrainAsync Aufruf beim Herunterfahren wartet auf alle, ohne dass bekannt ist, wie viele gestartet wurden oder wer sie gestartet hat. Der Tracker erzwingt auch eine konsistente Ausnahmebeobachtungsrichtlinie: Jede registrierte Aufgabe erhält die gleiche Fortsetzung der Fehlerprotokollierung, sodass keine Ausnahme unabhängig davon, welcher Codepfad die Arbeit gestartet hat, unbemerkt durchlaufen kann.

Die drei wichtigsten Komponenten des nachverfolgten Musters sind:

  • Zuweisen der Aufgabe zu einer Variablen – der Erhalt eines Verweises auf backgroundTask ermöglicht die Verfolgung. Eine Aufgabe, auf die Sie nicht verweisen können, ist eine Aufgabe, die Sie nicht entleeren oder überwachen können.
  • Registrieren Sie sich bei der Trackertracker.Track fügt die Fortsetzung der Fehlerprotokollierung an und fügt die Aufgabe dem In-Flight-Satz hinzu. Jede Ausnahme, die die Hintergrundarbeit auslöst, tritt durch diese Fortsetzung zutage, anstatt unbemerkt im Hintergrund zu verschwinden.
  • Abfluss beim Herunterfahrentracker.DrainAsync wartet auf alles, was noch läuft. Rufen Sie es auf, bevor Ihre Komponente oder Ihr Prozess beendet wird, um sicherzustellen, dass keine laufende Arbeit unvollständig bleibt.

Folgen nicht verfolgender Fire-and-Forget-Aktionen

Wenn Sie den zurückgegebenen Task anstatt ihn zu verfolgen verwerfen, führen Sie ein stilles Versagen herbei:

public static class FireAndForgetPitfall
{
    public static async Task RunAsync()
    {
        _ = Task.Run(async () =>
        {
            await Task.Delay(100);
            throw new InvalidOperationException("Background operation failed.");
        });

        await Task.Delay(150);
        Console.WriteLine("Caller finished without observing background completion.");
    }
}
Public Module FireAndForgetPitfall
    Public Async Function RunAsync() As Task
        Dim discardedTask As Task = Task.Run(Async Function()
                                                 Await Task.Delay(100)
                                                 Throw New InvalidOperationException("Background operation failed.")
                                             End Function)

        Await Task.Delay(150)
        Console.WriteLine("Caller finished without observing background completion.")
    End Function
End Module

Es folgen drei Probleme beim Ablegen der Aufgabe:

  • Stille Ausnahmen — das Ergebnis des InvalidOperationException Hintergrundvorgangs wird nie beobachtet. Die Laufzeit leitet sie bei der Finalisierung weiter UnobservedTaskException, was nicht deterministisch und viel zu spät ist, um es angemessen zu handhaben.
  • Keine Koordination beim Herunterfahren – der Anrufer läuft weiter und beendet sich, ohne auf den Abschluss des Vorgangs zu warten. Bei einem kurzlebigen Prozess oder einem Host mit einem Timeout zum Herunterfahren wird die Hintergrundarbeit abgebrochen oder geht vollständig verloren.
  • Keine Sichtbarkeit – ohne Einen Verweis auf die Aufgabe können Sie nicht ermitteln, ob der Vorgang erfolgreich war, fehlgeschlagen ist oder noch ausgeführt wird.

Untracked fire-and-forget ist nur zulässig, wenn alle drei der folgenden Bedingungen zutreffen: die Aufgabe wirklich optional ist, es sicher ist, das Scheitern zu ignorieren, und der Vorgang deutlich innerhalb der erwarteten Prozesslebensdauer abgeschlossen wird. Das Protokollieren eines nicht kritischen Telemetriepings ist ein Beispiel, bei dem alle diese Bedingungen erfüllt sein können.

Explizites Beibehalten des Besitzes

Verwenden Sie eines der folgenden Besitzmodelle:

  • Geben Sie das Task zurück und erfordern Sie, dass Aufrufer darauf warten.
  • Nachverfolgung von Hintergrundaufgaben in einem dedizierten Eigentümerdienst.
  • Verwenden Sie eine vom Host verwaltete Hintergrundabstraktion, sodass der Host die Lebensdauer besitzt.

Wenn die Arbeit nach der Rückkehr des Anrufers fortgesetzt werden muss, übertragen Sie den Besitz explizit. Beauftragen Sie beispielsweise einen Tracker, der Fehler protokolliert und am Systemherunterfahren beteiligt ist.

Anzeigen von Ausnahmen aus Hintergrundaufgaben

Verworfene Aufgaben können still fehlschlagen, bis der Abschluss erfolgt und die Behandlung unüberwachter Ausnahmen durchgeführt wird. Der Zeitpunkt ist unvorhersehbar und zu spät für die normale Bearbeitung von Anforderungen oder Workflows.

Fügen Sie beobachtungslogik an, wenn Sie Hintergrundaufgaben in die Warteschlange stellen. Mindestens protokollieren Sie Fehler in einer Fortsetzung. Verwenden Sie vorzugsweise einen zentralen Tracker, damit jede Warteschlangenoperation dieselbe Richtlinie erhält.

Details zur Ausbreitung von Ausnahmen finden Sie unter Aufgabenausnahmebehandlung.

Koordinieren des Abbruchs und Herunterfahrens

Binden Sie Hintergrundarbeit an ein Abbruchtoken, das die Lebensdauer der App oder des Vorgangs darstellt. Während des Abschaltens:

  1. Beenden Sie die Annahme neuer Arbeiten.
  2. Signalauslöschung.
  3. Erwarten Sie nachverfolgte Aufgaben mit einem begrenzten Timeout.
  4. Protokolliert unvollständige Vorgänge.

Dieser Ablauf hält die Abschaltung vorhersagbar und verhindert partielle Schreibvorgänge oder verwaiste Operationen.

Kann die GC eine asynchrone Methode sammeln, bevor sie abgeschlossen ist?

Die Laufzeit hält den asynchronen Zustandscomputer aktiv, während Fortsetzungen weiterhin darauf verweisen. Normalerweise verlieren Sie keinen asynchronen In-Flight-Vorgang zur Garbage Collection des Zustandscomputers selbst.

Sie können weiterhin die Richtigkeit verlieren, wenn Sie den Besitz des zurückgegebenen Vorgangs verlieren, erforderliche Ressourcen frühzeitig löschen oder den Prozess vor Abschluss beenden lassen. Konzentrieren Sie sich auf Aufgabeneigentum und koordiniertes Abschalten.

Siehe auch