Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Les tâches de type "tirer-et-oublier" sont faciles à lancer et facilement négligées. Si vous démarrez une opération asynchrone et supprimez le retour Task, vous perdez de la visibilité sur l’achèvement, l’annulation et les échecs.
La plupart des bogues de durée de vie dans le code asynchrone sont des bogues de propriété, et non des bogues du compilateur. La machine d'état async et son Task restent actifs tant que le travail est toujours accessible via des continuations. Des problèmes se produisent lorsque votre application ne suit plus ce travail.
Pourquoi le feu et l’oubli provoquent des bogues de durée de vie
Lorsque vous démarrez un travail en arrière-plan sans le suivre, vous créez trois risques :
- L’opération peut échouer et personne n’observe l’exception.
- Le processus ou l’hôte peut s’arrêter avant la fin de l’opération.
- L'opération peut perdurer au-delà de l'objet ou du périmètre qui était censé la contrôler.
Utilisez le mode "lancer et oublier" uniquement lorsque le travail est vraiment facultatif et que l’échec soit acceptable.
Suivre les tâches en arrière-plan de manière explicite
Cet exemple définit BackgroundTaskTracker une classe d’assistance personnalisée qui contient un dictionnaire sûr pour les threads des tâches en cours d'exécution. Lorsque vous appelez Track, il enregistre une continuation ContinueWith sur la tâche, qui supprime la tâche du dictionnaire une fois terminée et consigne toute défaillance. Lorsque vous appelez DrainAsync, il appelle Task.WhenAll pour chaque tâche encore présente dans le dictionnaire et renvoie la tâche ou le résultat correspondant.
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
L’exemple suivant utilise BackgroundTaskTracker pour démarrer, observer et vider une opération en arrière-plan :
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
Vous pourriez vous demander : si DrainAsync ne se contente d'attendre que la tâche que vous avez démarrée, pourquoi ne pas await backgroundTask directement et ignorer entièrement le traqueur ? Pour une tâche unique dans une seule méthode, vous pourriez le faire. Le suivi devient précieux lorsque les tâches sont démarrées à partir de nombreux endroits différents dans la durée de vie d’un composant. Chaque appelant transmet sa tâche au suivi partagé, et un seul DrainAsync appel lors de l'arrêt attend qu'ils aient tous terminé sans connaître leur nombre ou l'identité de ceux qui les ont lancés. Le traqueur applique également une stratégie d'observation des exceptions cohérente : chaque tâche enregistrée reçoit la même continuation de journalisation des échecs, afin qu'aucune exception ne puisse passer inaperçue, quel que soit le chemin de code qui a initié le travail.
Les trois composants clés du modèle suivi sont les suivants :
-
Affectez la tâche à une variable : gardez une référence à
backgroundTaskce qui rend le suivi possible. Une tâche à laquelle vous ne pouvez pas faire référence est une tâche que vous ne pouvez pas épuiser ou observer. -
Inscrivez-vous auprès du traceur :
tracker.Trackattache la continuation de la journalisation des défaillances et ajoute la tâche à l'ensemble en cours. Toute exception que le travail en arrière-plan lève apparaît à travers la continuation plutôt que de disparaître silencieusement. -
Vidage à l’arrêt :
tracker.DrainAsyncattend que tout soit en cours d’exécution. Appelez-le avant la sortie de votre composant ou processus pour garantir qu’aucun travail en cours d’exécution n’est abandonné à mi-vol.
Conséquences des systèmes « tir et oublie » non suivis
Si vous ignorez le retour Task au lieu de le suivre, vous créez un échec silencieux :
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
Trois problèmes suivent la suppression de la tâche :
-
Exceptions silencieuses : l’opération
InvalidOperationExceptionen arrière-plan n’est jamais observée. Le runtime l’achemine vers UnobservedTaskException lors de la finalisation, qui n’est pas déterministe et ce qui rend la gestion correcte impossible à ce stade. - Aucune coordination de l’arrêt : l’appelant continue et se ferme sans attendre la fin de l’opération. Sur un processus de courte durée ou un hôte avec un délai d’arrêt, le travail en arrière-plan est annulé ou perdu entièrement.
- Aucune visibilité : sans référence à la tâche, vous ne pouvez pas déterminer si l’opération a réussi, échoué ou est toujours en cours d’exécution.
Le tir sans suivi est acceptable uniquement lorsque les trois conditions suivantes sont remplies : le travail est véritablement facultatif, l'échec peut être ignoré sans danger, et l'opération se termine bien avant toute durée de vie de processus attendue. La journalisation d’un ping de télémétrie non critique est un exemple où ces conditions peuvent toutes être remplies.
Conserver la propriété explicite
Utilisez l’un des modèles de propriété suivants :
- Retournez le
Tasket exigez que les appelants l'attendent. - Effectuez le suivi des tâches en arrière-plan dans un service propriétaire dédié.
- Utilisez une abstraction de fond gérée par l'hôte afin que l'hôte ait le contrôle de la durée de vie.
Si le travail doit continuer après le retour de l’appelant, transférez explicitement la propriété. Par exemple, passez la tâche à un traceur qui enregistre automatiquement les erreurs et participe à la fermeture.
Afficher les exceptions des tâches en arrière-plan
Les tâches supprimées peuvent échouer en mode silencieux jusqu’à ce que la finalisation et la gestion des exceptions non observées se produisent. Ce minutage n’est pas déterministe et trop tard pour la gestion normale des requêtes ou des flux de travail.
Attachez une logique d’observation lorsque vous allez mettre en file d’attente une tâche en arrière-plan. À minima, enregistrer les échecs dans une suite. Préférez un suivi centralisé afin que chaque opération mise en file d’attente obtient la même stratégie.
Pour plus d’informations sur la propagation des exceptions, consultez gestion des exceptions de tâche.
Coordonner l’annulation et l’arrêt
Lier le travail en arrière-plan à un jeton d’annulation qui représente la durée de vie de l’application ou de l’opération. Pendant l’arrêt :
- Arrêtez d’accepter de nouveaux travaux.
- Signalez l’annulation.
- Attendez les tâches suivies avec un délai d’expiration limité.
- Journaliser les opérations incomplètes.
Ce flux maintient l’arrêt prévisible et empêche les écritures partielles ou les opérations orphelines.
Le GC peut-il collecter une méthode asynchrone avant qu'elle soit terminée ?
Le runtime maintient l’ordinateur d’état asynchrone actif pendant que les continuations le référencent toujours. Vous ne perdez généralement pas une opération asynchrone en cours à cause de la collecte de déchets de la machine d’état elle-même.
Vous pouvez toujours perdre la justesse si vous perdez la propriété de la tâche retournée, supprimez les ressources requises tôt ou laissez le processus se terminer avant l’achèvement. Concentrez-vous sur la propriété des tâches et l’arrêt coordonné.