Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Il lavoro avviato e dimenticato è facile da iniziare e facile da trascurare. Se si avvia un'operazione asincrona e si elimina l'oggetto restituito Task, si perde visibilità sul completamento, l'annullamento e gli errori.
La maggior parte dei bug persistenti nel codice asincrono sono legati alla gestione delle risorse, non bug del compilatore. La async macchina degli stati e il suo Task rimangono attivi finché il lavoro è ancora raggiungibile attraverso continuazioni. I problemi si verificano quando l'app non tiene più traccia del funzionamento.
Perché fire-and-forget causa bug legati al ciclo di vita
Quando si avvia il lavoro in background senza monitorarlo, si creano tre rischi:
- L'operazione può non riuscire e nessuno osserva l'eccezione.
- Il processo o l'host può essere arrestato prima del completamento dell'operazione.
- L'operazione può sopravvivere all'oggetto o all'ambito progettato per controllarlo.
Usare fire-and-forget solo quando il lavoro è veramente facoltativo e l'errore è accettabile.
Monitorare esplicitamente il lavoro in background
Questo esempio definisce BackgroundTaskTracker, una classe helper personalizzata che contiene un dizionario thread-safe di attività in esecuzione. Quando si invoca Track, registra una continuazione ContinueWith sull'attività che rimuove l'attività dal dizionario al termine e riporta eventuali errori. Quando si chiama DrainAsync, invoca Task.WhenAll su ogni attività ancora presente nel dizionario e restituisce l'attività risultante.
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'esempio seguente usa BackgroundTaskTracker per avviare, osservare e svuotare un'operazione in background:
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
Potresti chiederti: se DrainAsync aspetta solo l'attività che hai avviato, perché non await backgroundTask direttamente e ignora completamente il tracker? Per una singola attività in un singolo metodo, è possibile. Il tracker diventa utile quando le attività vengono avviate da molte posizioni diverse per tutta la durata di un componente. Ogni chiamante passa il proprio compito al tracker condiviso e una singola DrainAsync chiamata all'arresto li attende senza sapere quanti sono stati avviati o chi li ha avviati. Lo strumento di rilevamento applica anche un criterio di osservazione delle eccezioni coerente: ogni attività registrata ottiene la stessa continuazione di registrazione degli errori, quindi nessuna eccezione può scivolare attraverso un percorso non noto indipendentemente dal percorso di codice che ha avviato il lavoro.
I tre componenti chiave del modello tracciato sono:
-
Assegnare l'attività a una variabile : mantenere un riferimento a
backgroundTaskè ciò che rende possibile il rilevamento. Un'attività a cui non puoi riferirti è un'attività che non puoi completare o osservare. -
Eseguire la registrazione con il tracker :
tracker.Trackcollega la continuazione per il logging degli errori e aggiunge l'attività al set in esecuzione. Qualsiasi eccezione che il lavoro in background genera emerge attraverso quella continuazione anziché scomparire silenziosamente. -
Scarico all'arresto —
tracker.DrainAsyncrimane in attesa di tutto ciò che è ancora in esecuzione. Chiamalo prima dell'uscita del componente o del processo per garantire che nessun lavoro in volo venga abbandonato a metà volo.
Conseguenze di azioni non monitorate di tipo "fire-and-forget"
Se si rimuove l'oggetto restituito Task invece di monitorarlo, si crea un errore invisibile all'utente:
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
Tre problemi sono conseguenza dell'eliminazione dell'attività:
-
Eccezioni invisibili: il risultato
InvalidOperationExceptiondell'operazione in background non viene mai osservato. Il runtime lo indirizza a UnobservedTaskException durante la finalizzazione, che è non deterministica e troppo tardi per poter essere gestito correttamente. - Nessun coordinamento di arresto — il chiamante continua ed esce senza attendere il completamento dell'operazione. In un processo di breve durata o in un host con un timeout di spegnimento, il lavoro in background viene completamente annullato o perso.
- Nessuna visibilità : senza un riferimento all'attività, non è possibile determinare se l'operazione è riuscita, non è riuscita o è ancora in esecuzione.
Le operazioni non tracciate di tipo 'fire-and-forget' sono accettabili solo quando tutte e tre le seguenti condizioni sono soddisfatte: il lavoro è veramente facoltativo, il fallimento è sicuro da ignorare, e l'operazione viene completata correttamente entro qualsiasi durata prevista del processo. La registrazione di un ping di telemetria non critico è un esempio in cui tutte queste condizioni possono verificarsi.
Mantenere esplicita la proprietà
Usare uno di questi modelli di proprietà:
- Restituisci il
Taske richiedi ai chiamanti di attenderlo. - Tenere traccia delle attività in background in un servizio proprietario dedicato.
- Usare una gestione in background controllata dall'host affinché l'host sia responsabile della durata.
Se il lavoro deve continuare dopo la restituzione del chiamante, trasferire la proprietà in modo esplicito. Ad esempio, assegnare il compito a un tracker che registra gli errori e partecipa allo spegnimento.
Visualizzare eccezioni dalle attività in background
Le attività eliminate possono fallire in modo silente fino a quando non avviene la finalizzazione e la gestione delle eccezioni non rilevate. Tale intervallo è non deterministico e troppo tardi per la normale gestione della richiesta o del flusso di lavoro.
Collega la logica di osservazione quando metti in coda il lavoro in background. Come minimo, registrare gli errori in un processo continuato. Preferisci un tracciatore centralizzato in modo che ogni operazione in coda segua la stessa politica.
Per informazioni dettagliate sulla propagazione delle eccezioni, vedere Gestione delle eccezioni delle attività.
Coordinare la cancellazione e lo spegnimento
Associare il lavoro in background a un token di annullamento che rappresenta la durata dell'app o dell'operazione. Durante l'arresto:
- Interrompere l'accettazione di nuovi lavori.
- Annullamento del segnale.
- Attendi le attività tracciate con un timeout delimitato.
- Registrare le operazioni incomplete.
Questo flusso mantiene prevedibile l'arresto e impedisce operazioni parziali o orfane.
Il GC può raccogliere un metodo asincrono prima del completamento?
Il runtime mantiene attiva la macchina di stato asincrona finché le continuazioni fanno ancora riferimento a essa. In genere non si perde un'operazione asincrona in corso a causa della garbage collection della macchina a stati stessa.
È comunque possibile perdere la correttezza se si perde la proprietà dell'attività restituita, eliminare le risorse necessarie in anticipo o lasciare che il processo termini prima del completamento. Concentrarsi sulla responsabilità delle attività e sull'arresto coordinato.