Asynchrone methoden actief houden

Brand-en-vergeet werk is gemakkelijk te beginnen en gemakkelijk te verliezen. Als u een asynchrone bewerking start en de geretourneerde Task laat vallen of negeert, verliest u het zicht op de voltooiing, annulering en de fouten.

De meeste levensduurfouten in asynchrone code zijn eigendomsfouten, niet compilerfouten. De async toestandsmachine en zijn Task blijven in leven zolang het werk nog steeds bereikbaar is via continuaties. Er treden problemen op wanneer uw app dat werk niet meer bijhoudt.

Waarom systemen zonder verdere controle levenslange fouten kunnen veroorzaken

Wanneer u achtergrondwerk start zonder het bij te houden, maakt u drie risico's:

  • De bewerking kan mislukken en niemand ziet de uitzondering.
  • Het proces of de host kan worden afgesloten voordat de bewerking is voltooid.
  • De bewerking kan langer duren dan het object of het bereik dat bedoeld was om er controle over te houden.

Gebruik schiet-en-vergeet alleen als het werk echt optioneel is en de fout acceptabel is.

Achtergrondwerk expliciet bijhouden

In dit voorbeeld wordt een aangepaste helperklasse gedefinieerd BackgroundTaskTrackerdie een thread-veilige woordenlijst bevat van taken in vlucht. Wanneer u aanroept Track, wordt een ContinueWith vervolg geregistreerd voor de taak waarmee de taak uit de woordenlijst wordt verwijderd wanneer deze is voltooid en eventuele fouten registreert. Wanneer u DrainAsync aanroept, roept het Task.WhenAll aan op elke taak die nog in het woordenboek staat en de daaruit voortvloeiende taak wordt geretourneerd.

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

In het volgende voorbeeld wordt BackgroundTaskTracker gebruikt om een achtergrondbewerking te starten, te observeren en te stoppen.

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

U kunt zich afvragen: als DrainAsync enkel op de ene taak wacht die u bent begonnen, waarom niet direct await backgroundTask en de tracker helemaal overslaan? Voor één taak in één methode kunt u dat doen. De tracker wordt waardevol wanneer taken worden gestart vanaf veel verschillende plaatsen gedurende de levensduur van een onderdeel. Elke beller geeft de taak aan de gedeelde tracker en één DrainAsync aanroep bij het afsluiten wacht op hen allemaal zonder te weten hoeveel er zijn gestart of wie ze gestart heeft. Het volgprogramma dwingt ook een consistent beleid voor uitzonderingsobservatie af: elke geregistreerde taak krijgt dezelfde vastlegging van fouten, zodat geen enkele uitzondering onopgemerkt blijft, ongeacht welk codepad het werk heeft gestart.

De drie belangrijkste onderdelen van het bijgehouden patroon zijn:

  • Wijs de taak toe aan een variabele — het bijhouden van een verwijzing naar backgroundTask is wat het volgen mogelijk maakt. Een taak waarnaar u niet kunt verwijzen, is een taak die u niet kunt afhandelen of observeren.
  • Registreer u bij de tracker : tracker.Track koppelt de vervolgbewerking voor mislukte logboekregistratie en voegt de taak toe aan de in-flight-set. Elke uitzondering die het achtergrondproces opwerpt, wordt zichtbaar door die voortzetting in plaats van stilletjes te verdwijnen.
  • Afvoer bij afsluiten - tracker.DrainAsync wacht op alles dat nog actief is. Roep het aan voordat uw onderdeel of proces wordt afgesloten om te garanderen dat er geen in-flight werk wordt verlaten tijdens de vlucht.

Gevolgen van niet-bijgehouden brand en vergeet

Als u de geretourneerde Task negeert in plaats van deze bij te houden, leidt dit tot een stille fout:

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

Er volgen drie problemen bij het verwijderen van de taak:

  • Stille uitzonderingen — de InvalidOperationException van de achtergrondbewerking wordt nooit waargenomen. De runtime routeert deze naar UnobservedTaskException bij het voltooien, wat niet-deterministisch en veel te laat is om probleemloos te verwerken.
  • Geen afsluitingscoördinatie : de beller gaat verder en wordt afgesloten zonder te wachten tot de bewerking is voltooid. Bij een kortstondig proces of een host met een time-out voor afsluiten wordt het achtergrondwerk geannuleerd of volledig verloren gegaan.
  • Geen zichtbaarheid , zonder verwijzing naar de taak, kunt u niet bepalen of de bewerking is geslaagd, mislukt of nog steeds wordt uitgevoerd.

Niet-bijgehouden brand-en-vergeet is alleen acceptabel wanneer alle drie de volgende voorwaarden bevatten: het werk is echt optioneel, fout is veilig te negeren en de bewerking wordt goed voltooid binnen een verwachte levensduur van het proces. Het vastleggen van een niet-kritieke telemetrie ping is een voorbeeld waarin alle voorwaarden van toepassing kunnen zijn.

Eigendom expliciet houden

Gebruik een van deze eigendomsmodellen:

  • Retourneer de Task en vereis dat aanroepers deze afwachten.
  • Achtergrondtaken bijhouden in een speciale eigenarenservice.
  • Gebruik een door een host beheerde achtergrondabstractie, zodat de host eigenaar is van de levensduur.

Als het werk moet worden voortgezet nadat de beller terugkeert, moet u het eigendom expliciet overdragen. Geef de taak bijvoorbeeld aan een tracker die fouten registreert en deelneemt aan het afsluiten.

Surface uitzonderingen van achtergrondtaken zichtbaar maken

Gevallen taken kunnen stilletjes mislukken totdat de afhandeling van onopgemerkte uitzonderingen plaatsvindt. Deze timing is niet-deterministisch en te laat voor normale verwerking van aanvragen of werkstromen.

Voeg observatielogica toe wanneer u achtergrondwerk in de wachtrij zet. Log ten minste fouten tijdens een voortzetting. Geef de voorkeur aan een gecentraliseerde tracker, zodat elke bewerking in de wachtrij hetzelfde beleid krijgt.

Zie de afhandeling van taak-uitzonderingen voor details over het doorgeven van uitzonderingen.

Annulering en afsluiting coördineren

Koppel achtergrondwerk aan een annuleringstoken dat de levensduur van de app of bewerking aangeeft. Tijdens het afsluiten:

  1. Stop met het accepteren van nieuw werk.
  2. Signaalannulering.
  3. Wacht op bijgehouden taken met een gebonden time-out.
  4. Onvolledige bewerkingen vastleggen.

Dit proces zorgt voor een voorspelbare afsluiting en voorkomt gedeeltelijke schrijfbewerkingen of onvoltooide bewerkingen.

Kan de GC een asynchrone methode verzamelen voordat deze is voltooid?

De runtime houdt de asynchrone toestandsmachine actief zolang er nog referenties naar zijn. Meestal verliest u geen asynchrone bewerking tijdens een vlucht naar garbagecollection van de statusmachine zelf.

U kunt de juistheid nog steeds verliezen als u het eigendom van de geretourneerde taak kwijtraakt, de vereiste resources vroegtijdig weggooien of het proces laat eindigen voordat het is voltooid. Focus op taakeigendom en gecoördineerd afsluiten.

Zie ook