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 de fuego y olvido es fácil de empezar y fácil de perder. Si inicia una operación asincrónica y descarta el resultado devuelto del Task, pierde visibilidad sobre la finalización, la cancelación y los errores.
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 async máquina de estado y su Task se mantienen activas mientras el trabajo sigue siendo accesible mediante 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.
Utiliza "fire-and-forget" solo cuando el trabajo es realmente opcional y el fallo es aceptable.
Seguir explícitamente las tareas en segundo plano
En este ejemplo se define BackgroundTaskTracker, una clase auxiliar personalizada que contiene un diccionario seguro para hilos de tareas en curso. Cuando se llama a Track, registra una ContinueWith continuación en la tarea que quita la tarea del diccionario cuando se completa y registra cualquier error. Cuando se llama a DrainAsync, llama a Task.WhenAll en cada tarea que permanece en el diccionario y devuelve la tarea resultante correspondiente.
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
Puede preguntar: si DrainAsync solo espera la tarea que usted ha iniciado, ¿por qué no simplemente await backgroundTask y así omitir completamente el rastreador? Para una sola tarea en un único 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 llamador entrega su tarea al rastreador compartido y una sola DrainAsync llamada al apagado espera a todos ellos sin saber cuántos se iniciaron o quiénes los iniciaron. 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 purgar ni observar. -
Registrar con el rastreador —
tracker.Trackasocia la continuación del registro de errores y agrega la tarea al conjunto en curso. Cualquier excepción que el trabajo en segundo plano arroje se manifieste a través de esa continuación en lugar de desaparecer sin aviso. -
Purga en apagado :
tracker.DrainAsyncespera todo lo que sigue funcionando. Llámalo antes de que el componente o el proceso salgan para garantizar que no hay trabajo en vuelo abandonado a mitad de vuelo.
Consecuencias del "disparar y olvidar" 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 — la
InvalidOperationExceptionde la operación en segundo plano nunca se percibe. El tiempo de ejecución lo enruta a UnobservedTaskException en la finalización, que no es determinista y es demasiado tarde para gestionarlo 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 antes del 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.
Untracked fire-and-forget es aceptable solo cuando se cumplen las tres condiciones siguientes: el trabajo es realmente opcional, el error es seguro de ignorar, y la operación se completa bien dentro del tiempo de vida esperado de cualquier 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 en silencio hasta que se produzca la finalización y el manejo de excepciones no supervisado. Ese tiempo no es determinista y demasiado tarde para el control normal de solicitudes o flujos de trabajo.
Incluya la lógica de monitorización al encolar tareas en segundo plano. Como mínimo, registrar fallos en una serie de registros. 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.
Cancelación y apagado de coordenadas
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.
- Aguarda tareas de seguimiento con un límite de tiempo de espera.
- Registre operaciones incompletas.
Este flujo mantiene la predicción del apagado y evita escrituras parciales o 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 estado asincrónico mientras las continuaciones todavía hacen referencia a ella. Normalmente no se pierde una operación asincrónica en curso debido a la recolección de basura de la máquina de estados en sí misma.
Todavía puede haber una pérdida de precisión si pierde la propiedad de la tarea devuelta, libera los recursos necesarios prematuramente o permite que el proceso termine antes de completarse. Enfóquese en la responsabilidad de la tarea y del apagado coordinado.