Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
O trabalho de disparar e esquecer é fácil de começar e fácil de perder. Se iniciar uma operação assíncrona e descartar o resultado retornado Task, perde visibilidade sobre a conclusão, cancelamento e falhas.
A maioria dos erros durante o tempo de vida em código assíncrono são erros de propriedade, não de compiladores. A async máquina de estados e a sua Task permanecem ativas enquanto o trabalho ainda é alcançável através de continuações. Os problemas surgem quando a tua aplicação deixa de registar esse trabalho.
Porque é que o "disparar e esquecer" causa bugs permanentes
Quando começas o trabalho de fundo sem o acompanhar, crias três riscos:
- A operação pode falhar, e ninguém observa a exceção.
- O processo ou host pode desligar-se antes do término da operação.
- A operação pode sobreviver ao objeto ou escopo que deveria controlá-lo.
Use a abordagem de "disparar e esquecer" apenas quando o trabalho for realmente opcional e a falha for aceitável.
Acompanhe explicitamente o trabalho em segundo plano
Este exemplo define BackgroundTaskTracker, uma classe auxiliar personalizada que mantém um dicionário thread-safe de tarefas em voo. Quando chamas Track, regista uma ContinueWith continuação na tarefa que remove a tarefa do dicionário quando esta é concluída e regista qualquer falha. Quando chama DrainAsync, chama Task.WhenAll em cada tarefa ainda presente no dicionário e devolve 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 seguinte demonstra o uso de BackgroundTaskTracker para iniciar, monitorizar e drenar uma operação de fundo:
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
Podes perguntar: se DrainAsync só espera pela tarefa que começaste, porque não await backgroundTask diretamente e ignorar completamente o rastreador? Para uma única tarefa num único método, poderias. O rastreador torna-se valioso quando as tarefas são iniciadas a partir de muitos locais diferentes ao longo da vida útil de um componente. Cada chamador entrega a sua tarefa ao rastreador partilhado, e uma única DrainAsync chamada no desligamento espera todos sem saber quantos foram iniciados ou quem os iniciou. O tracker também aplica uma política consistente de observação de exceções: cada tarefa registada recebe a mesma continuação do registo de falhas, pelo que nenhuma exceção pode passar despercebida, independentemente do caminho do código que iniciou o trabalho.
Os três componentes principais do padrão monitorado são:
-
Atribuir a tarefa a uma variável — manter uma referência a
backgroundTaské o que torna possível acompanhar. Uma tarefa à qual não se pode referir é uma tarefa que não pode gerir ou observar. -
Registar com o rastreador —
tracker.Trackanexa a continuação do registo de falhas e adiciona a tarefa ao conjunto em voo. Qualquer exceção lançada pelo trabalho de fundo torna-se visível através dessa continuação, em vez de desaparecer silenciosamente. -
Drenagem ao desligar —
tracker.DrainAsyncespera que tudo ainda esteja a funcionar. Avise antes que o seu componente ou processo saia para garantir que nenhum trabalho em voo é abandonado a meio do voo.
Consequências de sistemas de disparo automático sem acompanhamento
Se você descartar o valor retornado Task em vez de o rastrear, 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
Três problemas resultam de abandonar a tarefa:
-
Exceções silenciosas — a
InvalidOperationExceptionoperação a partir do segundo plano nunca é observada. O tempo de execução encaminha-o para UnobservedTaskException na finalização, o que não é determinístico e demasiado tarde para ser tratado com elegância. - Sem coordenação de encerramento — o chamador continua e sai sem esperar que a operação termine. Num processo efémero ou num host com tempo limite de encerramento, o trabalho em segundo plano é cancelado ou perdido por completo.
- Sem visibilidade — sem uma referência à tarefa, não se pode determinar se a operação teve sucesso, falhou ou ainda está a decorrer.
O disparar e esquecer sem rastreio só é aceitável quando as três condições seguintes se confirmam: o trabalho é genuinamente opcional, a falha é segura para ignorar e a operação é concluída dentro de qualquer tempo de vida útil esperado do processo. Registar um ping de telemetria não crítico é um exemplo em que estas condições podem ser cumpridas.
Mantenha a propriedade explícita
Use um destes modelos de propriedade:
- Devolva o
Taske exija que os chamadores aguardem. - Acompanha tarefas em segundo plano num serviço dedicado ao proprietário.
- Use uma abstração de segundo plano gerida pelo anfitrião para que o anfitrião tenha o ciclo de vida.
Se o trabalho tiver de continuar após o retorno do chamador, transfira explicitamente a propriedade. Por exemplo, entrega a tarefa a um rastreador que regista erros e participa no desligamento.
Expôr exceções de tarefas em segundo plano
Tarefas canceladas podem falhar silenciosamente até que ocorra a finalização e o tratamento de exceções não observadas. Esse timing não é determinístico e já é tarde demais para o tratamento normal de pedidos ou fluxos de trabalho.
Anexa lógica de observação quando colocares trabalho em segundo plano. Pelo menos, registe falhas numa continuação. Prefiro um rastreador centralizado para que todas as operações em fila tenham a mesma política.
Para detalhes sobre propagação de exceções, veja Gestão de exceções de tarefa.
Coordenação de cancelamento e encerramento
Associe trabalho em segundo plano a um token de cancelamento que represente a vida útil da aplicação ou operação. Durante o encerramento:
- Deixa de aceitar novos trabalhos.
- Cancelamento de sinal.
- Aguardar tarefas acompanhadas com um tempo limite limitado.
- Registar operações incompletas.
Este fluxo mantém o encerramento previsível e previne escritas parciais ou operações órfãs.
O GC consegue recolher um método assíncrono antes de terminar?
O runtime mantém a máquina de estados assíncrona ativa enquanto as continuações continuam a referi-la. Normalmente, não se perde uma operação assíncrona durante a execução devido à coleta de lixo da máquina de estado em si.
Ainda pode perder a exatidão se perder a propriedade da tarefa devolvida, eliminar os recursos necessários prematuramente ou deixar o processo finalizar antes de estar concluído. Foque-se na responsabilidade da tarefa e no encerramento coordenado.