Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
"Ett arbete av typen 'starta och glöm' är lätt att påbörja och lätt att glömma bort." Om du startar en asynkron åtgärd och släpper den returnerade Taskförlorar du insyn i slutförande, annullering och fel.
De flesta livsbuggar i asynkron kod är ägarfel, inte kompilatorbuggar. Tillståndsmaskinen async och dess Task förblir aktiva så länge arbetet fortfarande är tillgängligt genom fortsättningar. Problem uppstår när appen inte längre spårar det arbetet.
Varför eld och glöm orsakar livstidsbuggar
När du startar bakgrundsarbete utan att spåra det skapar du tre risker:
- Åtgärden kan misslyckas och ingen observerar undantaget.
- Processen eller värddatorn kan stängas av innan operationen är klar.
- Åtgärden kan överleva det objekt eller omfång som var avsett att styra det.
Använd endast "eld och glöm"-läget när arbetet verkligen är valfritt och det är acceptabelt att misslyckas.
Spåra bakgrundsarbete explicit.
Det här exemplet definierar BackgroundTaskTracker, en anpassad hjälpklass som innehåller en trådsäker ordlista med uppgifter under flygning. När du anropar Trackregistreras en ContinueWith fortsättning på uppgiften som tar bort aktiviteten från ordlistan när den är klar och loggar eventuella fel. När du anropar DrainAsync, anropas Task.WhenAll på varje uppgift som fortfarande finns med i ordlistan och den resulterande uppgiften returneras.
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
I följande exempel används BackgroundTaskTracker för att starta, observera och tömma en bakgrundsåtgärd:
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
Du kanske frågar: om DrainAsync bara väntar på den enda uppgift du startade, varför inte await backgroundTask direkt och hoppa över spåraren helt? För en enskild uppgift inom en enda metod kan du göra så. Spåraren blir värdefull när uppgifter startas från många olika platser under en komponents livslängd. Varje uppringare ger sin uppgift till den delade spåraren, och ett enda DrainAsync samtal vid avstängning väntar på dem alla utan att veta hur många som startades eller vem som startade dem. Spåraren tillämpar också en konsekvent princip för undantagsövervakning: varje registrerad uppgift får samma fortsättning på felloggning, så inget undantag kan glida igenom obemärkt oavsett vilken kodsökväg som startade arbetet.
De tre viktigaste komponenterna i det spårade mönstret är:
-
Tilldela uppgiften till en variabel – att hålla en referens till
backgroundTaskär det som gör spårning möjlig. En uppgift som du inte kan referera till är en uppgift som du inte kan avsluta eller observera. -
Registrera dig med spåraren –
tracker.Trackbifogar fortsättningen för felloggning och lägger till uppgiften i uppsättningen under flygning. Alla undantag som bakgrundsarbetet kastar hanteras genom den fortsättningen hellre än att försvinna tyst. -
Töm vid avstängning –
tracker.DrainAsyncväntar på allt som fortfarande körs. Anropa den innan komponenten eller processen avslutas för att garantera att inget arbete under flygning avbryts mitt under flygning.
Konsekvenser av ospårat "fire-and-forget"
Om du ignorerar returen Task i stället för att spåra den skapar du en tyst felaktighet:
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
Tre problem uppstår från att släppa uppgiften:
-
Tysta undantag –
InvalidOperationExceptionfrån bakgrundsåtgärden observeras aldrig. Körmiljön dirigerar den till UnobservedTaskException vid slutförandet, vilket är icke-deterministiskt och alldeles för sent för att hanteras smidigt. - Ingen avstängningssamordning – anroparen fortsätter och avslutas utan att vänta på att åtgärden ska slutföras. Vid en kortvarig process eller en värd med en tidsgräns för avstängning avbryts eller förloras bakgrundsarbetet helt.
- Ingen synlighet – utan en referens till uppgiften kan du inte avgöra om åtgärden lyckades, misslyckades eller fortfarande körs.
Ospårad eld och glöm är bara acceptabelt när alla tre av följande villkor gäller: arbetet är verkligen valfritt, fel är säkert att ignorera och åtgärden slutförs väl inom en förväntad processlivslängd. Att logga en icke-kritisk telemetripuls är ett exempel där alla dessa villkor kan gälla.
Behåll ägarskapet explicit
Använd någon av dessa ägarskapsmodeller:
- Återlämna
Taskoch kräv att anropare väntar på det. - Spåra bakgrundsprocesser i en dedikerad ägartjänst.
- Använd en värdhanterad bakgrundsabstraktion så att värden äger livslängden.
Om arbetet måste fortsätta efter att anroparen har returnerat överför du ägarskapet explicit. Tilldela till exempel uppgiften till ett spårningsverktyg som loggar fel och är delaktig vid avstängningen.
Surface undantag från bakgrundsaktiviteter
Förlorade uppgifter kan misslyckas tyst tills slutförande och obemärkt undantagshantering inträffar. Den tidpunkten är icke-deterministisk och för sent för normal hantering av begäranden eller arbetsflöden.
Lägg till observationslogik när du köar bakgrundsarbete. Logga åtminstone fel i en fortsättningsprocess. Föredra ett centraliserat spårningsverktyg så att varje köad åtgärd följer samma policy.
Information om undantagsspridning finns i Hantering av task-undantag.
Samordna annullering och avstängning
Koppla bakgrundsarbete till en annulleringstoken som representerar appens eller åtgärdens livslängd. Under avstängning:
- Sluta acceptera nytt arbete.
- Signaldämpning.
- Vänta på spårade uppgifter med en begränsad tidsgräns.
- Logga ofullständiga åtgärder.
Det här flödet håller avstängningen förutsägbar och förhindrar partiella skrivningar eller överblivna åtgärder.
Kan GC samla in en asynkron metod innan den är klar?
Körningstiden håller den asynkrona tillståndsmaskinen vid liv medan fortsättningarna fortfarande refererar till den. Du förlorar vanligtvis inte en asynkron åtgärd under flygning till skräpinsamling av själva tillståndsdatorn.
Du kan fortfarande förlora korrekthet om du förlorar ägarskapet för den returnerade uppgiften, tar bort nödvändiga resurser tidigt eller låter processen avslutas innan den slutförs. Fokusera på aktivitetsägarskap och samordnad avstängning.