Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O trabalho "disparar e esquecer" é fácil de iniciar e fácil de perder. Se você iniciar uma operação assíncrona e descartar o Task retornado, você perde a visibilidade da conclusão, cancelamento e falhas.
A maioria dos bugs relativos ao tempo de vida no código assíncrono são bugs relacionados à propriedade, não bugs do compilador. A máquina de estados async e seu Task permanecem ativos enquanto o trabalho ainda for alcançável por meio de continuações. Problemas acontecem quando seu aplicativo não controla mais esse trabalho.
Por que o "disparar e esquecer" causa bugs de ciclo de vida?
Ao iniciar o trabalho em segundo plano sem rastreá-lo, você cria três riscos:
- A operação pode falhar e ninguém observa a exceção.
- O processo ou host pode ser desligado antes que a operação seja concluída.
- A operação pode sobreviver ao objeto ou escopo que deveria controlá-la.
Use "disparar e esquecer" somente quando o trabalho for realmente opcional e a falha for aceitável.
Monitorar processos em segundo plano de forma explícita
Este exemplo define BackgroundTaskTracker, uma classe auxiliar personalizada que mantém um dicionário thread-safe de tarefas em andamento. Quando você chama Track, ele registra uma continuação ContinueWith na tarefa que remove a tarefa do dicionário quando ela for concluída e registra qualquer falha. Quando você chama DrainAsync, ele chama Task.WhenAll em todas as tarefas ainda no dicionário e retorna a tarefa 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
O exemplo a seguir usa BackgroundTaskTracker para iniciar, observar e esvaziar uma operação em 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
Você pode perguntar: se DrainAsync apenas aguarda a tarefa que você iniciou, por que não await backgroundTask diretamente e ignorar o rastreador completamente? Para uma única tarefa em um único método, você poderia. O rastreador se torna valioso quando as tarefas são iniciadas de muitos lugares diferentes ao longo do tempo de vida de um componente. Cada chamador passa sua tarefa para o rastreador compartilhado, e uma única chamada DrainAsync no encerramento aguarda todas elas sem saber quantas foram iniciadas ou quem as iniciou. O rastreador também impõe uma política de observação de exceção consistente: cada tarefa registrada obtém a mesma continuação de log de falhas, portanto, nenhuma exceção pode passar despercebida independentemente de qual caminho de código iniciou o trabalho.
Os três principais componentes do padrão rastreado são:
-
Atribuir a tarefa a uma variável – manter uma referência
backgroundTaské o que possibilita o acompanhamento. Uma tarefa à qual você não pode se referir é uma tarefa que você não pode drenar ou observar. -
Registre-se com o rastreador —
tracker.Trackanexa a continuação de registro de falhas e adiciona a tarefa ao conjunto em andamento. Qualquer exceção lançada pelo trabalho em segundo plano se manifesta através dessa continuação, em vez de desaparecer silenciosamente. -
Drenar no encerramento —
tracker.DrainAsyncaguarda tudo o que ainda estiver em execução. Chame-o antes que seu componente ou processo termine para garantir que nenhum trabalho em andamento seja abandonado no meio da execução.
Consequências do disparo e esquecimento não rastreado
Se você descartar o Task retornado em vez de rastreá-lo, você cria uma falha silenciosa:
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
Problemas decorrentes do abandono da tarefa:
-
Exceções silenciosas — o
InvalidOperationExceptionda operação em segundo plano nunca é observado. O tempo de execução o direciona para UnobservedTaskException na finalização, o que não é determinístico e é muito tarde para ser tratado adequadamente. - Nenhuma coordenação de desligamento – a chamada continua e termina sem esperar a conclusão da operação. Em um processo de curta duração ou em um host com um tempo limite de desligamento, o trabalho em segundo plano é cancelado ou perdido completamente.
- Sem visibilidade – sem uma referência à tarefa, você não pode determinar se a operação foi bem-sucedida, falhou ou ainda está em execução.
A execução assíncrona sem rastreamento é aceitável somente quando todas as três condições a seguir forem verdadeiras: o trabalho é genuinamente opcional, a falha pode ser ignorada com segurança e a operação é concluída bem dentro do tempo de vida esperado do processo. Registrar um ping de telemetria não crítico é um exemplo em que todas essas condições podem ser mantidas.
Manter a propriedade explícita
Use um destes modelos de propriedade:
- Retorne o
Taske exija que os chamadores o aguardem. - Acompanhe as tarefas em segundo plano em um serviço de proprietário dedicado.
- Use uma abstração de segundo plano gerenciada pelo host para que ele controle o ciclo de vida.
Se o trabalho precisar continuar depois que o chamador retornar, transfira a propriedade explicitamente. Por exemplo, entregue a tarefa a um rastreador que registra erros e participa do desligamento.
Exibir exceções de tarefas em segundo plano
Tarefas descartadas podem falhar silenciosamente até que a finalização e o tratamento de exceções não observadas ocorram. Esse tempo é não determinístico e tarde demais para tratamento normal de solicitação ou fluxo de trabalho.
Anexe lógica de observação ao enfileirar trabalho em segundo plano. No mínimo, registre as falhas em uma continuação. Prefira um rastreador centralizado para que todas as operações enfileiradas recebam a mesma política.
Para obter informações sobre a propagação de exceções, consulte Tratamento de exceção de tarefa.
Coordenação do cancelamento e encerramento
Vincule o trabalho em segundo plano a um token de cancelamento que representa o tempo de vida do aplicativo ou da operação. Durante o desligamento:
- Pare de aceitar novos trabalhos.
- Cancelamento de sinal.
- Aguarde tarefas rastreadas com um tempo limite limitado.
- Log de operações incompletas.
Esse fluxo mantém o desligamento previsível e evita gravações parciais ou operações órfãs.
O GC pode coletar um método assíncrono antes de terminar?
O runtime mantém a máquina de estado assíncrona ativa enquanto as continuações ainda fazem referência a ela. Geralmente, você não perde uma operação assíncrona em andamento devido à coleta de lixo da própria máquina de estados.
Você ainda poderá perder a corretude se perder o controle da tarefa retornada, descartar o recurso necessário antecipadamente ou permitir que o processo termine antes de sua finalização. Concentre-se na propriedade da tarefa e no desligamento coordenado.