Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Práce typu "zadání a zapomenutí" je snadná začít a snadno ji opomenout. Pokud spustíte asynchronní operaci a nevyužijete vrácenou hodnotu Task, přijdete o možnost sledovat dokončení, zrušení a selhání.
Většina chyb životnosti v asynchronním kódu představuje chyby vlastnictví, nikoli chyby kompilátoru. Stavový async automat a jeho Task zůstávají naživu, zatímco práce je stále dosažitelná prostřednictvím pokračování. K problémům dochází, když vaše aplikace už danou práci nesleduje.
Proč metoda fire-and-forget způsobuje chyby související s životností
Když spustíte práci na pozadí bez sledování, vytvoříte tři rizika:
- Operace může selhat a nikdo nepozoruje výjimku.
- Proces nebo hostitel se může vypnout dříve, než se operace dokončí.
- Operace může překonat časové omezení objektu nebo rozsahu, který ji měl řídit.
Používejte princip "vystřel a zapomeň" pouze tehdy, je-li práce skutečně volitelná a selhání je přijatelné.
Explicitní sledování práce na pozadí
Tato ukázka definuje vlastní pomocnou třídu BackgroundTaskTracker, která obsahuje slovník úkolů, jež jsou bezpečně řízeny pomocí vláken. Při volání Track zaregistruje ContinueWith pokračovací akci na úkolu, která po dokončení odebere úlohu ze slovníku a zaloguje případná selhání. Při volání DrainAsync se volá Task.WhenAll na každý úkol, který je ještě ve slovníku, a vrátí tak výsledný úkol.
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
Následující příklad používá BackgroundTaskTracker ke spuštění, sledování a ukončení procesu na pozadí:
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
Můžete se zeptat: Pokud DrainAsync jen čeká na jeden úkol, který jste zahájili, proč ne await backgroundTask přímo a zcela ignorovat tracker? Pro jeden úkol v jedné metodě můžete. Sledování se stává cenným, když jsou úkoly zahájeny z mnoha různých míst v průběhu životnosti komponenty. Každý volající předá svůj úkol sdílenému sledovači, a jedna DrainAsync výzva při vypnutí čeká na všechny, bez ohledu na to, kolik bylo spuštěno, nebo protože není známo, kdo je spustil. Sledování také vynucuje konzistentní zásady pozorování výjimek: každý zaregistrovaný úkol získá stejné pokračování protokolu o selhání, takže žádná výjimka nemůže uniknout bez povšimnutí, bez ohledu na to, která cesta kódu zahájila úkol.
Mezi tři klíčové součásti sledovaného vzoru patří:
-
Přiřaďte úkol proměnné – udržování odkazu na
backgroundTaskumožňuje sledování. Úkol, na který nemůžete odkazovat, je úkol, který nemůžete dokončit nebo pozorovat. -
Zaregistrujte se u sledovacího systému –
tracker.Trackpřipojuje pokračování záznamu selhání a přidává úlohu do sady probíhajících úloh. Jakákoli výjimka, kterou práce na pozadí vyhodí, je zachycena pokračováním, spíše než aby zmizela potichu. -
Vyprázdnění při vypnutí –
tracker.DrainAsyncčeká na všechny stále aktivní procesy. Zavolejte ho před ukončením komponenty nebo procesu, abyste zajistili, že v polovině letu nebude zrušena žádná práce v letu.
Důsledky nesledovaného požáru a zapomenutí
Pokud vrácenou hodnotu Task zahodíte místo sledování, vytvoříte tiché selhání:
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
Tři problémy následují od vyřazení úkolu:
-
Tiché výjimky – operace
InvalidOperationExceptionna pozadí není nikdy zaznamenána. Modul běhu ho směruje na UnobservedTaskException při dokončení, což je nedeterministické a příliš pozdě, aby to bylo možné elegantně zpracovat. - Žádná koordinace vypnutí – volající pokračuje bez čekání na dokončení operace a ukončí činnost. V krátkodobém procesu nebo na hostiteli, kde vyprší úroveň vypnutí, se práce na pozadí zruší nebo zcela zmizí.
- Žádná viditelnost – bez odkazu na úlohu nemůžete určit, jestli operace proběhla úspěšně, selhala nebo je stále spuštěná.
Neřízené operace typu fire-and-forget jsou přijatelné pouze tehdy, když platí všechny tři z následujících podmínek: práce je opravdu volitelná, na selhání lze bezpečně zapomenout, a operace se dokončí dobře během očekávané životnosti procesu. Protokolování nekritického telemetrického pingu je jedním z příkladů, kdy všechny tyto podmínky můžou platit.
Zachování explicitního vlastnictví
Použijte jeden z těchto modelů vlastnictví:
-
TaskVraťte a vyžadujte, aby jej volající čekali. - Sledování úloh na pozadí ve službě vyhrazeného vlastníka
- Použijte abstrakci na pozadí spravovanou hostitelem, aby hostitel řídil životnost procesu.
Pokud musí práce pokračovat po vrácení volajícího, přeneste vlastnictví explicitně. Úkol můžete například předat nástroji pro sledování, který protokoluje chyby a podílí se na procesu vypnutí.
Zobrazit výjimky z úloh na pozadí
Vyřazené úlohy mohou tiše selhat, dokud nedojde k dokončení a zpracování nezachycených výjimek. Toto načasování není deterministické a příliš pozdě pro běžné zpracování požadavků nebo pracovních postupů.
Připojte logiku pozorování při zařazování práce do fronty na pozadí. Minimálně zaznamenávejte selhání při pokračování procesu. Preferujte centralizovaný tracker, aby každá operace ve frontě získala stejnou politiku.
Podrobnosti o šíření výjimek naleznete v tématu Zpracování výjimek úlohy.
Koordinace zrušení a vypnutí
Spojte práci na pozadí s tokenem zrušení, který představuje životnost aplikace nebo operace. Během vypnutí:
- Přestaňte přijímat novou práci.
- Zrušení signálu.
- Čekejte na sledované úkoly s ohraničeným časovým limitem.
- Protokolovat neúplné operace
Tento tok udržuje předvídatelné vypnutí a zabraňuje částečným zápisům nebo osiřelým operacím.
Může GC shromáždit asynchronní metodu před dokončením?
Modul runtime udržuje asynchronní stavový počítač naživu, zatímco pokračování na něj stále odkazují. K uvolnění paměti samotného stavového počítače obvykle nepřijdete o asynchronní operaci v letu.
Správnost můžete i nadále ztratit, pokud ztratíte vlastnictví vráceného úkolu, vyřaďte požadované prostředky včas nebo nechte proces před dokončením ukončit. Zaměřte se na zodpovědnost za úkoly a koordinované uzavírání.