Qual è lo scopo del "ritorno in attesa" in C #?


251

Esiste uno scenario in cui il metodo di scrittura come questo:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

Invece di questo:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

avrebbe senso?

Perché usare il return awaitcostrutto quando puoi tornare direttamente Task<T>dall'invocazione interna DoAnotherThingAsync()?

Vedo il codice con return awaitin così tanti posti, penso che potrei essermi perso qualcosa. Ma per quanto ho capito, non usare parole chiave asincrone / wait in questo caso e restituire direttamente l'attività sarebbe funzionalmente equivalente. Perché aggiungere un sovraccarico aggiuntivo di awaitlivello aggiuntivo ?


2
Penso che l'unica ragione per cui lo vedi sia perché le persone imparano per imitazione e generalmente (se non ne hanno bisogno) usano la soluzione più semplice che riescono a trovare. Quindi le persone vedono quel codice, usano quel codice, vedono che funziona e da ora in poi, per loro, è il modo giusto di farlo ... Non serve in quel caso aspettare
Fabio Marcolini,

7
C'è almeno una differenza importante: la propagazione delle eccezioni .
noseratio,

1
Nemmeno io lo capisco, non riesco affatto a comprendere l'intero concetto, non ha alcun senso. Da quello che ho imparato se un metodo ha un tipo restituito, DEVE avere una parola chiave return, non sono le regole del linguaggio C #?
monstro

@monstro la domanda del PO ha però la dichiarazione di ritorno?
David Klempfner,

Risposte:


189

C'è un caso subdolo quando returnnel metodo normale e return awaitnel asyncmetodo si comportano diversamente: quando combinato con using(o, più in generale, qualsiasi return awaitin un tryblocco).

Considera queste due versioni di un metodo:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Il primo metodo farà Dispose()l' Foooggetto non appena il DoAnotherThingAsync()metodo ritorna, il che è probabilmente molto tempo prima che venga effettivamente completato. Ciò significa che la prima versione è probabilmente difettosa (perché Fooè disposta troppo presto), mentre la seconda versione funzionerà bene.


4
Per completezza, nel primo caso dovresti tornarefoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord

7
@ghord Che non funzionerebbe, Dispose()ritorna void. Avresti bisogno di qualcosa del genere return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Ma non so perché dovresti farlo quando puoi usare la seconda opzione.
svick,

1
@svick Hai ragione, dovrebbe essere più sulla falsariga di { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Il caso d'uso è piuttosto semplice: se sei su .NET 4.0 (come la maggior parte), puoi comunque scrivere codice asincrono in questo modo che funzionerà bene chiamato dalle app 4.5.
ghord

2
@ghord Se sei su .Net 4.0 e vuoi scrivere un codice asincrono, probabilmente dovresti usare Microsoft.Bcl.Async . E il tuo codice viene eliminato Foosolo dopo il Taskcompletamento del reso , cosa che non mi piace, perché introduce inutilmente la concorrenza.
svick,

1
@svick Il tuo codice attende fino al completamento anche dell'attività. Inoltre, Microsoft.Bcl.Async è inutilizzabile per me a causa della dipendenza da KB2468871 e dei conflitti quando si utilizza la base di codice asincrono .NET 4.0 con il codice asincrono 4.5 corretto.
ghord

93

Se non è necessario async(ovvero, è possibile restituire Taskdirettamente), quindi non utilizzare async.

Ci sono alcune situazioni in cui return awaitè utile, come se avessi due operazioni asincrone da fare:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

Per ulteriori informazioni sulle asyncprestazioni, consultare l' articolo MSDN di Stephen Toub e il video sull'argomento.

Aggiornamento: ho scritto un post sul blog che approfondisce molti dettagli.


13
Potresti aggiungere una spiegazione sul perché awaitè utile nel secondo caso? Perché non farlo return SecondAwait(intermediate);?
Matt Smith,

2
Ho la stessa domanda di Matt, non return SecondAwait(intermediate);raggiungerei l'obiettivo anche in quel caso? Penso che return awaitsia ridondante anche qui ...
TX_

23
@MattSmith Quello non si sarebbe compilato. Se si desidera utilizzare awaitnella prima riga, è necessario utilizzarlo anche nella seconda riga.
svick,

2
@cateyes Non sono sicuro di cosa significhi "sovraccarico introdotto dal parallelismo", ma la asyncversione utilizzerà meno risorse (thread) rispetto alla versione sincrona.
svick,

4
@TomLint Non si compila davvero. Supponendo che il tipo restituito SecondAwaitsia `stringa, il messaggio di errore è:" CS4016: poiché si tratta di un metodo asincrono, l'espressione di ritorno deve essere di tipo 'stringa' piuttosto che 'Attività <stringa>' ".
svick,

23

L'unico motivo per cui vorresti farlo è se ce n'è un altro awaitnel codice precedente o se stai in qualche modo manipolando il risultato prima di restituirlo. Un altro modo in cui ciò potrebbe accadere è attraverso un try/catchche cambia il modo in cui vengono gestite le eccezioni. Se non stai facendo nulla di tutto ciò, allora hai ragione, non c'è motivo di aggiungere il sovraccarico di fare il metodo async.


4
Come per la risposta di Stephen, non capisco perché sarebbe return awaitnecessario (invece di restituire semplicemente il compito di invocazione del figlio) anche se c'è qualche altra attesa nel codice precedente . La prego di fornire una spiegazione?
TX_

10
@TX_ Se si desidera rimuovere, asynccome si attende la prima attività? È necessario contrassegnare il metodo come asyncse si desidera utilizzare qualsiasi attesa. Se il metodo è contrassegnato come asynce si dispone di un awaitcodice precedente, è necessario eseguire awaitla seconda operazione asincrona affinché sia ​​del tipo corretto. Se lo avessi appena rimosso, awaitnon verrebbe compilato in quanto il valore restituito non sarebbe del tipo corretto. Poiché il metodo è asyncil risultato è sempre racchiuso in un'attività.
Servito il

11
@Noseratio Prova i due. Le prime compilazioni. Il secondo no. Il messaggio di errore ti dirà il problema. Non restituirai il tipo corretto. Quando in un asyncmetodo non si restituisce un'attività, si restituisce il risultato dell'attività che verrà quindi racchiuso.
Servito il

3
@Servy, ovviamente - hai ragione. In quest'ultimo caso ritorneremmo Task<Type>esplicitamente, mentre è asyncimposto il ritorno Type(che il compilatore stesso trasformerebbe in Task<Type>).
noseratio,

3
@Itsik Certo, asyncè solo zucchero sintattico per collegare esplicitamente le continuazioni. Non c'è bisogno async di fare nulla, ma quando si fa praticamente qualsiasi operazione asincrona non banale E ' drammaticamente più facile lavorare con. Ad esempio, il codice che hai fornito in realtà non propaga gli errori come vorresti, e farlo correttamente in situazioni ancora più complesse inizia a diventare molto più difficile. Anche se non hai mai bisogno async , le situazioni che descrivo sono quelle in cui è un valore aggiunto per usarlo.
Servito il

17

Un altro caso che potrebbe essere necessario attendere il risultato è questo:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

In questo caso, GetIFooAsync()deve attendere il risultato di GetFooAsyncperché il tipo di Tè diverso tra i due metodi e Task<Foo>non è direttamente assegnabile a Task<IFoo>. Ma se si attendono il risultato, diventa solo Fooche è direttamente assegnabile a IFoo. Quindi il metodo asincrono riconfeziona il risultato dentro Task<IFoo>e via.


1
D'accordo, questo è davvero fastidioso - credo che la causa sottostante Task<>sia invariante.
StuartLC,

7

Rendere asincrono il metodo "thunk" altrimenti semplice crea una macchina a stati asincroni in memoria, mentre quella non asincrona no. Mentre ciò può spesso indicare alla gente di utilizzare la versione non asincrona perché è più efficiente (il che è vero) significa anche che in caso di blocco, non hai prove che quel metodo sia coinvolto nello "stack di ritorno / continuazione" che a volte rende più difficile capire l'hang.

Quindi sì, quando perf non è critico (e di solito non lo è) lancerò asincrono su tutti questi metodi thunk in modo da avere la macchina a stati asincrona per aiutarmi a diagnosticare blocchi in seguito, e anche per garantire che se quelli i metodi thunk si sono sempre evoluti nel tempo, saranno sicuri di restituire compiti difettosi invece di lanciare.


6

Se non utilizzerai return wait, potresti rovinare la traccia dello stack durante il debug o quando viene stampata nei log sulle eccezioni.

Quando si restituisce l'attività, il metodo ha raggiunto il suo scopo ed è fuori dallo stack di chiamate. Quando lo usi return awaitlo lasci nello stack di chiamate.

Per esempio:

Stack di chiamata quando si utilizza wait: A in attesa dell'attività da B => B in attesa dell'attività da C

Call stack quando non si utilizza wait: A in attesa dell'attività da C, che B ha restituito.


2

Anche questo mi confonde e sento che le risposte precedenti hanno trascurato la tua vera domanda:

Perché utilizzare il valore return wait costrutto quando è possibile restituire direttamente Task dall'invocazione DoAnotherThingAsync () interna?

Beh, a volte in realtà vuoi un Task<SomeType>, ma la maggior parte delle volte in realtà vuoi un'istanza di SomeType, cioè, il risultato dell'attività .

Dal tuo codice:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Una persona che non ha familiarità con la sintassi (io, ad esempio) potrebbe pensare che questo metodo dovrebbe restituire a Task<SomeResult>, ma poiché è contrassegnato da async, significa che il suo tipo di ritorno effettivo è SomeResult. Se lo usi return foo.DoAnotherThingAsync(), restituiresti un'attività che non verrà compilata. Il modo corretto è di restituire il risultato dell'attività, quindi il return await.


1
"tipo di ritorno effettivo". Eh? async / await non sta cambiando i tipi di ritorno. Nel tuo esempio var task = DoSomethingAsync();ti darebbe un compito, nonT
Scarpa

2
@Schoe Non sono sicuro di aver capito bene la async/awaitcosa. Per quanto ne so Task task = DoSomethingAsync(), mentre Something something = await DoSomethingAsync()entrambi funzionano. Il primo ti dà l'attività corretta, mentre il secondo, grazie alla awaitparola chiave, ti dà il risultato dall'attività dopo che è stata completata. Potrei, per esempio, avere Task task = DoSomethingAsync(); Something something = await task;.
Heltonbiker,
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.