Az aszinkron metódusok életben tartása

Könnyen elkezdhető és könnyen elhanyagolható munka. Ha elindít egy aszinkron műveletet, és figyelmen kívül hagyja a visszaadott Task, elveszíti a láthatóságot a befejezésre, lemondásra és hibákra vonatkozóan.

Az aszinkron kód legtöbb élettartamú hibája tulajdonosi hiba, nem fordítóhibák. Az async állapotgép és a Task életben maradnak, amíg a munka folytatások révén elérhető. Problémák akkor fordulnak elő, ha az alkalmazás már nem követi nyomon a működést.

Miért okoz a tűz és a felejtés életre szóló hibákat?

Ha nyomon követés nélkül kezdi el a háttérmunkát, három kockázattal jár:

  • A művelet sikertelen lehet, és senki sem veszi figyelembe a kivételt.
  • A folyamat vagy a kiszolgáló leállhat a művelet befejeződése előtt.
  • A művelet képes túllépni a vezérlésére szánt objektumon vagy hatókörön.

Csak akkor használja a tűz és felejtés módszert, ha a munka valóban opcionális, és a kudarc elfogadható.

Az háttérmunka explicit nyomon követése

Ez a minta egy egyéni segédosztályt határoz meg BackgroundTaskTracker, amely a repülés közbeni feladatok szálbiztos szótárát tartalmazza. Amikor meghívja a Track-t, regisztrál egy ContinueWith folytatást azon a feladaton, amely befejezésekor eltávolítja a feladatot a szótárból és naplózza az összes hibát. DrainAsyncHíváskor a függvény meghívja Task.WhenAll a szótárban lévő összes feladatot, és visszaadja az eredményül kapott feladatot.

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

Az alábbi példa a BackgroundTaskTracker használatával indítja, figyeli meg és zárja le a háttérműveletet.

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

Felteheti a kérdést: ha DrainAsync csak arra a feladatra vár, amelyet elindított, miért ne await backgroundTask közvetlenül és teljesen elkerülhetnénk a nyomkövetőt? Egyetlen metódus egyetlen feladata esetén megteheti. A nyomkövető akkor válik értékessé, ha az összetevők élettartama során számos különböző helyről indulnak el a tevékenységek. Minden hívó átadja a feladatát a megosztott nyomkövetőnek, és a leállításkor egyetlen DrainAsync hívás várja az összeset anélkül, hogy tudná, hányan indultak el, vagy ki indította el őket. A nyomkövető egységes kivételmegfigyelési szabályzatot is kikényszerít: minden regisztrált tevékenység ugyanazt a hibanaplózási folytatást kapja, így egyetlen kivétel sem csúszhat észrevétlenül, függetlenül attól, hogy melyik kódútvonal indította el a munkát.

A nyomon követett minta három fő összetevője a következő:

  • Rendelje hozzá a feladatot egy változóhoz – a nyomon követés lehetővé teszi a hivatkozás backgroundTask megtartását. A nem hivatkozható feladat olyan feladat, amelyet nem lehet üríteni vagy megfigyelni.
  • Regisztráljon a nyomkövetőveltracker.Track csatolja a hibanaplózás folytatását, és hozzáadja a feladatot a repülés közbeni készlethez. A háttérmunka által dobott kivételek ezen a folytatáson keresztül jelennek meg, ahelyett, hogy csendben eltűnnének.
  • Leállításkor ürítéstracker.DrainAsync megvárja, amíg minden leáll. Hívja meg az összetevő vagy a folyamat kilépése előtt, hogy garantálhassa, hogy a repülés közbeni munka ne legyen megszakítva a repülés közben.

Az önirányító (fire-and-forget) rendszerek következményei nyomon követés nélkül

Ha a visszaküldött Task értéket elveti ahelyett, hogy nyomon követné, csendes meghibásodást eredményez.

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

A feladat elvetése három problémát okoz:

  • Csendes kivételek – a InvalidOperationException háttérművelet nem figyelhető meg. A futtatókörnyezet a véglegesítéskor - ami nem determinisztikus - irányítja át UnobservedTaskException-ba, ami már túl késő lenne a megfelelő kezeléshez.
  • Nincs leállítási koordináció – a hívó a művelet befejezésére való várakozás nélkül folytatja és kilép. Egy rövid élettartamú folyamat vagy egy leállítási időtúllépéssel rendelkező gazdagép esetén a háttérmunka teljesen megszakad vagy elveszik.
  • Nincs láthatóság – a tevékenységre való hivatkozás nélkül nem állapítható meg, hogy a művelet sikeres volt-e, sikertelen volt-e, vagy még mindig fut.

A nem nyomon követett tűz és felejtés csak akkor elfogadható, ha az alábbi feltételek közül mind a három feltétel teljesül: a munka valóban nem kötelező, a hiba nyugodtan figyelmen kívül hagyható, és a művelet minden várt folyamat élettartama alatt jól befejeződik. A nem kritikus telemetriai ping naplózása az egyik példa arra, hogy ezek a feltételek mind megtarthatók.

Azonosítsa világosan a tulajdonjogot.

Használja az alábbi tulajdonosi modellek egyikét:

  • Adja vissza a Task-t, és kötelezze a hívókat annak kivárására.
  • Háttérfeladatok nyomon követése egy dedikált tulajdonosi szolgáltatásban.
  • Használjon gazdagép által kezelt háttér absztrakciót, hogy a gazdagép az élettartam fölött rendelkezzen.

Ha a hívó visszatérése után a munkának folytatódnia kell, akkor a tulajdonjogot kifejezetten át kell ruháznia. Adja át például a feladatot egy nyomkövetőnek, amely naplózza a hibákat, és részt vesz a leállításban.

Surface háttérfeladatok kivételei

Az elvetett tevékenységek csendesen meghiúsulhatnak, amíg a véglegesítés és a kivétel nélküli kezelés nem történik meg. Ez az időzítés nem determinisztikus, és túl késő a normál kérés- vagy munkafolyamat-kezeléshez.

Amikor háttérmunkát várakoztatsz, csatold a megfigyelési logikát. Legalább naplózza a hibákat a folytatásban. Válasszon egy központosított nyomkövetőt, így minden várólistán lévő művelet ugyanazt a szabályzatot kapja.

A kivételpropagálás részleteiért lásd: Feladat kivételkezelése.

A lemondás és a leállítás koordinálása

A háttérmunkát egy lemondási tokenhez kapcsoljuk, amely az alkalmazás vagy a művelet élettartamát képviseli. Leállítás során:

  1. Hagyja abba az új munka elfogadását.
  2. Jelkioltás.
  3. Várja meg a nyomon követett feladatokat korlátozott időkerettel.
  4. Hiányos műveletek naplózása.

Ez a folyamat kiszámíthatóvá teszi a leállítást, és megakadályozza a részleges írásokat vagy az önállóan maradt műveleteket.

A csoportházirend-objektum összegyűjthet egy aszinkron metódust, mielőtt befejeződik?

A futtatókörnyezet életben tartja az aszinkron állapotú gépet, miközben a folytatások továbbra is hivatkoznak rá. Általában nem veszíti el a repülés közbeni aszinkron műveletet az állapotgép szemétgyűjtése miatt.

Elveszítheti a helyességet, ha elveszíti a visszaadott feladat tulajdonjogát, idő előtt megsemmisíti a szükséges erőforrásokat, vagy hagyja, hogy a folyamat még befejezése előtt végződjön le. Összpontosítson a tevékenység tulajdonjogára és az összehangolt leállításra.

Lásd még