Differenza tra await e ContinueWith


119

Qualcuno può spiegare se awaite ContinueWithsono sinonimi o meno nel seguente esempio. Sto cercando di utilizzare TPL per la prima volta e ho letto tutta la documentazione, ma non capisco la differenza.

Aspetta :

String webText = await getWebPage(uri);
await parseData(webText);

Continua con :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

Uno è preferito rispetto all'altro in situazioni particolari?


3
Se è stato rimosso il Waitchiamata nel secondo esempio poi i due frammenti sarebbero (in gran parte) equivalente.
Servizio


FYI: il tuo getWebPagemetodo non può essere utilizzato in entrambi i codici. Nel primo codice ha un Task<string>tipo di ritorno mentre nel secondo ha un stringtipo di ritorno. quindi fondamentalmente il tuo codice non si compila. - se per essere precisi.
Royi Namir

Risposte:


101

Nel secondo codice, stai aspettando in modo sincrono il completamento della continuazione. Nella prima versione, il metodo tornerà al chiamante non appena raggiunge la prima awaitespressione che non è già stata completata.

Sono molto simili in quanto entrambi programmano una continuazione, ma non appena il flusso di controllo diventa anche leggermente complesso, awaitporta a un codice molto più semplice. Inoltre, come notato da Servy nei commenti, l'attesa di un'attività "scarterà" le eccezioni aggregate che di solito portano a una gestione più semplice degli errori. Anche l'utilizzo awaitpianificherà implicitamente la continuazione nel contesto chiamante (a meno che non venga utilizzato ConfigureAwait). Non è niente che non possa essere fatto "manualmente", ma è molto più facile farlo await.

Ti suggerisco di provare a implementare una sequenza di operazioni leggermente più ampia con entrambi awaite Task.ContinueWith- può aprire gli occhi.


2
Anche la gestione degli errori tra i due frammenti è diversa; in genere è più facile lavorare con awaitover ContinueWitha questo proposito.
Servizio

@Servy: Vero, aggiungerà qualcosa al riguardo.
Jon Skeet

1
Anche la programmazione è abbastanza diversa, ovvero il contesto in cui parseDataviene eseguito.
Stephen Cleary,

Quando dici che l' uso di await programmerà implicitamente la continuazione nel contesto della chiamata , puoi spiegare il vantaggio di ciò e cosa succede nell'altra situazione?
Harrison

4
@ Harrison: Immagina di scrivere un'app WinForms - se scrivi un metodo asincrono, per impostazione predefinita tutto il codice all'interno del metodo verrà eseguito nel thread dell'interfaccia utente, perché la continuazione sarà pianificata lì. Se non si specifica dove si desidera eseguire la continuazione, non so quale sia l'impostazione predefinita ma potrebbe facilmente finire per funzionare su un thread del pool di thread ... a quel punto non è possibile accedere all'interfaccia utente, ecc. .
Jon Skeet

100

Ecco la sequenza di frammenti di codice che ho usato di recente per illustrare la differenza e vari problemi utilizzando le soluzioni asincrone.

Supponi di avere un gestore di eventi nella tua applicazione basata su GUI che richiede molto tempo e quindi desideri renderlo asincrono. Ecco la logica sincrona con cui inizi:

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem restituisce un'attività, che alla fine produrrà alcuni risultati che desideri esaminare. Se il risultato corrente è quello che stai cercando, aggiorni il valore di un contatore sull'interfaccia utente e torni dal metodo. In caso contrario, continui a elaborare più elementi da LoadNextItem.

Prima idea per la versione asincrona: usa solo le continuazioni! E ignoriamo la parte in loop per il momento. Voglio dire, cosa potrebbe andare storto?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

Ottimo, ora abbiamo un metodo che non blocca! Invece si blocca. Eventuali aggiornamenti ai controlli dell'interfaccia utente dovrebbero essere eseguiti nel thread dell'interfaccia utente, quindi sarà necessario tenerne conto. Per fortuna, c'è un'opzione per specificare come dovrebbero essere pianificate le continuazioni, e ce n'è una predefinita solo per questo:

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Ottimo, ora abbiamo un metodo che non si blocca! Invece fallisce silenziosamente. Le continuazioni sono esse stesse attività separate, con il loro stato non legato a quello dell'attività antecedente. Quindi, anche se LoadNextItem ha un errore, il chiamante vedrà solo un'attività che è stata completata con successo. Ok, quindi trasmetti l'eccezione, se ce n'è una:

return LoadNextItem().ContinueWith(t => {
    if (t.Exception != null) {
        throw t.Exception.InnerException;
    }
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Fantastico, ora funziona davvero. Per un singolo articolo. Ora, che ne dici di quel loop. Si scopre che una soluzione equivalente alla logica della versione sincrona originale sarà simile a questa:

Task AsyncLoop() {
    return AsyncLoopTask().ContinueWith(t =>
        Counter.Value = t.Result,
        TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
    var tcs = new TaskCompletionSource<int>();
    DoIteration(tcs);
    return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
    LoadNextItem().ContinueWith(t => {
        if (t.Exception != null) {
            tcs.TrySetException(t.Exception.InnerException);
        } else if (t.Result.Contains("target")) {
            tcs.TrySetResult(t.Result.Length);
        } else {
            DoIteration(tcs);
        }});
}

Oppure, invece di tutto quanto sopra, puoi usare async per fare la stessa cosa:

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

Adesso è molto più carino, no?


Grazie, davvero bella spiegazione
Elger Mensonides

Questo è un ottimo esempio
Royi Namir
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.