2015-06-25 7 views
9

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.

+0

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 .... –

+0

@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

+0

'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

risposta

2

Io uso anche IsDisposed per verificare lo stato del controllo in tali situazioni. Sebbene sia un po 'prolisso, non è più prolisso del necessario per gestire la situazione - e non è affatto confuso. Un linguaggio funzionale come F # con le monadi potrebbe probabilmente aiutare qui - non sono esperto - ma questo sembra buono come succede in C#.

2

Dovrebbe essere abbastanza semplice avere un CancellationTokenSource di proprietà del modulo e il modulo chiamare Cancel quando viene chiuso.

Quindi i metodi async possono osservare CancellationToken.

+0

Questo è il modo giusto per farlo. Inoltre qualsiasi logica dovrebbe essere fattorizzata dal modulo (e dall'unità testata). I gestori di eventi del modulo devono essere nudi e devono chiamare direttamente i metodi asincroni su un modello o servizio di visualizzazione, passando il metodo al token di annullamento. – jnm2

+1

Ho pensato a questo, ma ero preoccupato per una possibile condizione di gara. Quindi su 'FormClosing' chiamo' Annulla' sul token. Ma il 'Task' ha già completato e pubblicato la sua continuazione (il resto del metodo dopo l'attesa) per il' SynchronizationContext' catturato. Che in questo caso si trova nella coda dei messaggi e continuerà a funzionare. Suppongo di poterlo facilmente testare saturando pesantemente la coda dei messaggi. – Zer0

+0

C'è davvero una condizione di gara facile da riprodurre qui. Questo è peggio sia del mio design che del suggerimento di impedire la chiusura del modulo perché ora ci sono eccezioni non gestite. Posso pubblicare il codice che ho usato per testare se lo desideri. – Zer0

0

Una volta ho risolto un problema simile non chiudendo il modulo. Invece, l'ho nascosto all'inizio e l'ho chiuso solo quando tutti i lavori in sospeso erano terminati. Ho dovuto tenere traccia di quel lavoro, ovviamente, sotto forma di variabili Task.

Trovo che questa sia una soluzione pulita perché i problemi di smaltimento non si presentano affatto. Tuttavia, l'utente può chiudere immediatamente il modulo.