Considerate questo codice che viene eseguito sul thread UI:Come meglio gestire i controlli disposti quando si utilizza async/attendono
dividends = await Database.GetDividends();
if (IsDisposed)
return;
//Do expensive UI work here
earnings = await Database.GetEarnings();
if (IsDisposed)
return;
//Do expensive UI work here
//etc...
Nota che ogni volta che await
ho anche controllare IsDisposed
. È necessario perché dire che I await
su un lungo periodo Task
. Nel frattempo l'utente chiude il modulo prima che venga completato. Il Task
termina ed esegue una continuazione che tenta di accedere ai controlli su un modulo disposto. Si verifica un'eccezione
C'è un modo migliore per gestire questo o semplificare questo modello? Io uso await
liberamente nel codice UI ed è sia brutto da controllare per IsDisposed
ogni volta e soggetto a errori se non ricordo.
EDIT:
Ci sono alcune soluzioni proposte che non si adattano il disegno di legge perché cambiano funzionalità.
- Prevenire forma di chiusura fino a quando le operazioni in background completi
Ciò vanificare gli utenti. Inoltre, consente comunque di eseguire operazioni GUI potenzialmente costose che è una perdita di tempo, fa male alle prestazioni e non è più rilevante. Nel caso in cui quasi sempre lavori in background questo potrebbe impedire la chiusura del modulo per un tempo molto lungo.
- nascondere il modulo e chiudere una volta tutti i compiti completi
Questo ha tutti i problemi di prevenire la forma stretta eccezione non vanificare gli utenti. Le continuazioni che fanno un costoso lavoro sulla GUI continueranno a funzionare. Aggiunge anche la complessità del tracciamento quando tutte le attività vengono completate e quindi chiude il modulo se è nascosto.
- Utilizzare un
CancellationTokenSource
di annullare tutti i compiti quando la forma si sta chiudendo
Questo non ha nemmeno affrontare il problema. In effetti, lo faccio già (inutile sprecare risorse di background). Questa non è una soluzione perché ho ancora bisogno di controllare IsDisposed
a causa di una condizione di razza implicita. Il codice seguente dimostra le condizioni della gara.
public partial class NotMainForm : Form
{
private readonly CancellationTokenSource tokenSource = new CancellationTokenSource();
public NotMainForm()
{
InitializeComponent();
FormClosing += (sender, args) => tokenSource.Cancel();
Load += NotMainForm_Load;
Shown += (sender, args) => Close();
}
async void NotMainForm_Load(object sender, EventArgs e)
{
await DoStuff();
}
private async Task DoStuff()
{
try
{
await Task.Run(() => SimulateBackgroundWork(tokenSource.Token), tokenSource.Token);
}
catch (TaskCanceledException)
{
return;
}
catch (OperationCanceledException)
{
return;
}
if (IsDisposed)
throw new InvalidOperationException();
}
private void SimulateBackgroundWork(CancellationToken token)
{
Thread.Sleep(1);
token.ThrowIfCancellationRequested();
}
}
La corsa condizione si verifica quando l'attività è già completato, il modulo è chiusa, e la continuazione corre ancora. Verrà visualizzato il messaggio InvalidOperationException
ogni tanto. Annullare l'attività è una buona pratica, certo, ma non mi allevia dal dover controllare IsDisposed
.
CHIARIMENTO
L'esempio di codice originale è esattamente quello che voglio in termini di funzionalità. È solo un brutto schema e fare "attendere il lavoro in background per poi aggiornare la GUI" è un caso d'uso abbastanza comune. Tecnicamente parlando voglio solo che la continuazione non venga eseguita affatto se il modulo è disposto.Il codice di esempio fa proprio questo, ma non elegantemente ed è soggetto a errori (se ho dimenticato di controllare IsDisposed
su ogni singolo await
sto introducendo un bug). Idealmente, voglio scrivere un wrapper, un metodo di estensione, ecc. Che possa incapsulare questo progetto di base. Ma non riesco a pensare a un modo per farlo.
Inoltre, suppongo che io debba dichiarare che la prestazione è una considerazione di prima classe. Lanciare un'eccezione, ad esempio, è molto costosa per motivi a cui non entrerò. Quindi anche io non voglio provare a catturare lo ObjectDisposedException
ogni volta che faccio un await
. Anche codice più brutto e fa male anche le prestazioni. Sembra che fare un controllo ogni IsDisposed
ogni volta sia la soluzione migliore ma vorrei che ci fosse un modo migliore.
EDIT # 2
quanto riguarda le prestazioni - sì, è tutto relativo. Capisco che la stragrande maggioranza degli sviluppatori non si preoccupa dei costi di lancio delle eccezioni. Il vero costo del lancio di un'eccezione è fuori tema. Ci sono molte informazioni disponibili su questo altrove. Basti dire che sono molti ordini di grandezza più costosi rispetto al controllo if (IsDisposed)
. Per me, il costo dell'eliminazione inutile delle eccezioni è inaccettabile. In questo caso dico inutile perché ho già una soluzione che non genera eccezioni. Ancora una volta, lasciare che una continuazione passi a ObjectDisposedException
non è una soluzione accettabile e esattamente quello che sto cercando di evitare.
Hmya, questo è un problema standard di DoEvents(). Async/await non fa nulla per risolverlo se chiudendo la finestra non si chiude anche l'app. Potresti impedire all'utente di chiudere la finestra .... –
@HansPassant Certo, potrei farlo. Ma poi avrei solo un gruppo di utenti arrabbiati in attesa di un modulo da aggiornare che vogliono chiudere. Inoltre, l'esempio è notevolmente semplificato. Potrei avere molte attività in esecuzione che catturavano il contesto dell'interfaccia utente in qualsiasi momento. Il codice sopra è quello che voglio funzionalmente ma ora ho questo modello "se non disposto continua" che infesta il mio codice base. – Zer0
'OnFormClosing' è possibile nascondere il modulo e quindi eliminarlo dopo il completamento dei dati. Anche se il tuo codice farà un lavoro inutile, a meno che tu non abbia un 'bool isStopRequested;' (o qualcosa di simile) condiviso. – Loathing