Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
El trabajo "fire-and-forget" es fácil de iniciar y fácil de perder. Si inicia una operación asíncrona y descarta el Task devuelto, pierde visibilidad sobre la finalización, la cancelación y los fallos.
La mayoría de los errores de ciclo de vida en el código asincrónico son errores de propiedad y no del compilador. La máquina de estados async y su Task permanecen activos mientras el trabajo siga siendo accesible a través de continuaciones. Los problemas se producen cuando la aplicación ya no realiza un seguimiento de ese trabajo.
¿Por qué "fire-and-forget" causa errores persistentes?
Al iniciar el trabajo en segundo plano sin realizar un seguimiento, se crean tres riesgos:
- La operación puede producir un error y nadie observa la excepción.
- El proceso o el host se pueden apagar antes de que finalice la operación.
- La operación puede sobrevivir al objeto o al ámbito que se suponía que debía controlarla.
Utilice "fire-and-forget" solo cuando el trabajo sea verdaderamente opcional y el fallo sea aceptable.
Seguir explícitamente las tareas en segundo plano
Este ejemplo define BackgroundTaskTracker, una clase auxiliar personalizada que contiene un diccionario seguro para subprocesos con las tareas en curso. Al llamar a Track, se registra una continuación ContinueWith en la tarea que elimina esta del diccionario cuando se completa y registra cualquier error. Al llamar a DrainAsync, se invoca Task.WhenAll en cada tarea que aún se encuentra en el diccionario y se devuelve la tarea resultante.
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
En el ejemplo siguiente se usa BackgroundTaskTracker para iniciar, observar y purgar una operación en segundo plano:
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
Podrías preguntarte: si DrainAsync solo espera la única tarea que iniciaste, ¿por qué no llamar directamente a await backgroundTask y omitir por completo el rastreador? Para una sola tarea en un solo método, podría hacerlo. El rastreador resulta útil cuando las tareas se inician desde muchos lugares diferentes a lo largo de la duración de un componente. Cada llamante entrega su tarea al rastreador compartido, y una única llamada a DrainAsync al apagar el sistema las espera a todas sin saber cuántas se iniciaron ni quién las inició. El rastreador también aplica una directiva coherente de observación de excepciones: cada tarea registrada obtiene la misma continuación de registro de errores, por lo que ninguna excepción puede pasar desapercibida independientemente de la ruta de acceso del código que inició el trabajo.
Los tres componentes clave del patrón de seguimiento son:
-
Asignar la tarea a una variable : mantener una referencia a
backgroundTaskes lo que hace posible el seguimiento. Una tarea a la que no se puede hacer referencia es una tarea que no se puede vaciar ni observar. -
Registrarse en el rastreador —
tracker.Trackadjunta la continuación de registro de fallos y añade la tarea al conjunto de tareas en curso. Cualquier excepción que lance el trabajo en segundo plano se muestra a través de esa continuación en lugar de desaparecer silenciosamente. -
Drenar al apagar —
tracker.DrainAsyncespera a que termine todo lo que aún se esté ejecutando. Llámelo antes de que su componente o proceso se cierre para garantizar que ningún trabajo en curso quede abandonado a mitad de camino.
Consecuencias del "fire-and-forget" sin seguimiento
Si descarta el valor devuelto Task en lugar de hacerle seguimiento, crea un error silencioso.
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
Tres problemas provienen de abandonar la tarea:
-
Excepciones silenciosas — el
InvalidOperationExceptionde la operación en segundo plano nunca se observa. El tiempo de ejecución lo redirige a UnobservedTaskException en la finalización, lo cual es no determinista y llega demasiado tarde para administrarlo correctamente. - Sin coordinación de apagado : el autor de la llamada continúa y sale sin esperar a que finalice la operación. En un proceso de corta duración o en un host con un tiempo de espera de apagado, el trabajo en segundo plano se cancela o se pierde por completo.
- Sin visibilidad : sin referencia a la tarea, no se puede determinar si la operación se ha realizado correctamente, ha producido un error o sigue ejecutándose.
El modelo "fire-and-forget" sin seguimiento solo es aceptable cuando se cumplen las tres condiciones siguientes: la tarea es realmente opcional, el fallo puede ignorarse sin riesgo y la operación se completa con holgura dentro de la duración prevista del proceso. El registro de un ping de telemetría no crítico es un ejemplo en el que todas estas condiciones pueden cumplirse.
Mantener la propiedad explícita
Use uno de estos modelos de propiedad:
- Devuelve
Tasky requiere que los invocadores lo esperen. - Realice un seguimiento de las tareas en segundo plano en un servicio de propietario dedicado.
- Use una abstracción en segundo plano gestionada por el host para que este controle el ciclo de vida.
Si el trabajo debe continuar después de que el llamador regrese, transfiera la propiedad explícitamente. Por ejemplo, envíe la tarea a un rastreador que registra errores y participa en el apagado.
Mostrar excepciones provenientes de tareas en segundo plano
Las tareas descartadas pueden fallar de forma silenciosa hasta que se produzca la finalización y el manejo de excepciones no observadas. Ese tiempo no es determinista y demasiado tarde para el control normal de solicitudes o flujos de trabajo.
Incorpore lógica de observación al poner en cola tareas en segundo plano. Como mínimo, registre los fallos en una continuación. Es preferible un rastreador centralizado para que todas las operaciones en cola obtengan la misma política.
Para obtener detalles sobre la propagación de excepciones, consulte Control de excepciones de tareas.
Coordine la cancelación y el apagado
Vincular el trabajo en segundo plano a un token de cancelación que representa la duración de la aplicación o de la operación. Durante el apagado:
- Deje de aceptar el nuevo trabajo.
- Cancelación de señal.
- Espere a las tareas rastreadas con un tiempo de espera limitado.
- Registre operaciones incompletas.
Este flujo mantiene el apagado predecible y evita escrituras parciales u operaciones huérfanas.
¿Puede el GC recopilar un método asincrónico antes de que finalice?
El tiempo de ejecución mantiene activa la máquina de estados asíncrona mientras las continuaciones sigan haciendo referencia a ella. Normalmente no se pierde una operación asíncrona en curso debido a la recolección de basura de la propia máquina de estados.
Aún así, puede perder la corrección si pierde la propiedad de la tarea devuelta, libera los recursos necesarios antes de tiempo o deja que el proceso finalice antes de completarse. Enfóquese en la responsabilidad de la tarea y del apagado coordinado.