Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
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
backgroundTaskmegtartását. A nem hivatkozható feladat olyan feladat, amelyet nem lehet üríteni vagy megfigyelni. -
Regisztráljon a nyomkövetővel –
tracker.Trackcsatolja 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és –
tracker.DrainAsyncmegvá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
InvalidOperationExceptionhá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:
- Hagyja abba az új munka elfogadását.
- Jelkioltás.
- Várja meg a nyomon követett feladatokat korlátozott időkerettel.
- 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.