¿Cuál es el propósito de "retorno espera" en C #?

251

¿Hay algún escenario en el que escribir un método como este:

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

en lugar de esto:

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

tendría sentido?

¿Por qué usar return awaitconstruct cuando puedes regresar directamente Task<T>de la DoAnotherThingAsync()invocación interna ?

Veo código return awaiten tantos lugares, creo que podría haber perdido algo. Pero por lo que entiendo, no usar palabras clave asíncronas / esperar en este caso y devolver directamente la Tarea sería funcionalmente equivalente. ¿Por qué agregar sobrecarga adicional de awaitcapa adicional ?

TX_
fuente
2
Creo que la única razón por la que ves esto es porque las personas aprenden por imitación y generalmente (si no lo necesitan) usan la solución más simple que pueden encontrar. Entonces la gente ve ese código, usa ese código, ve que funciona y de ahora en adelante, para ellos, esa es la forma correcta de hacerlo ... No sirve esperar en ese caso
Fabio Marcolini
77
Hay al menos una diferencia importante: la propagación de excepciones .
noseratio
1
Tampoco lo entiendo, no puedo comprender todo este concepto, no tiene ningún sentido. De lo que aprendí si un método tiene un tipo de retorno, DEBE tener una palabra clave de retorno, ¿no son las reglas del lenguaje C #?
monstruo
@monstro la pregunta del OP tiene la declaración de retorno sin embargo?
David Klempfner

Respuestas:

189

Hay un caso disimulado cuando returnen el método normal y return awaiten asynccomportan método diferente: cuando se combina con using(o, más en general, cualquier return awaiten un trybloque).

Considere estas dos versiones de un método:

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

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

El primer método será Dispose()el Fooobjeto tan pronto como el DoAnotherThingAsync()método regrese, lo que probablemente sea mucho antes de que realmente se complete. Esto significa que la primera versión probablemente tenga errores (porque Foose desecha demasiado pronto), mientras que la segunda versión funcionará bien.

svick
fuente
44
Para completar, en el primer caso deberías regresarfoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord
77
@ghord Eso no funcionaría, Dispose()regresa void. Necesitarías algo como eso return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Pero no sé por qué harías eso cuando puedes usar la segunda opción.
svick
1
@svick Tienes razón, debería ser más como { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. El caso de uso es bastante simple: si está en .NET 4.0 (como la mayoría), aún puede escribir código asíncrono de esta manera, que funcionará muy bien desde 4.5 aplicaciones.
ghord
2
@ghord Si está en .Net 4.0 y desea escribir código asincrónico, probablemente debería usar Microsoft.Bcl.Async . Y su código se desecha Foosolo después de que se Taskcomplete la devolución , lo que no me gusta, porque innecesariamente introduce concurrencia.
svick
1
@svick Tu código espera hasta que la tarea también termine. Además, Microsoft.Bcl.Async es inutilizable para mí debido a la dependencia de KB2468871 y los conflictos al usar la base de código asíncrona .NET 4.0 con el código asíncrono 4.5 adecuado.
ghord
93

Si no lo necesita async(es decir, puede devolverlo Taskdirectamente), no lo use async.

Hay algunas situaciones en las que return awaites útil, como si tiene dos operaciones asincrónicas que hacer:

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

Para obtener más información sobre el asyncrendimiento, consulte el artículo y el video de MSDN de Stephen Toub sobre el tema.

Actualización: he escrito una publicación de blog que entra en mucho más detalle.

Stephen Cleary
fuente
13
¿Podría agregar una explicación de por qué awaites útil en el segundo caso? ¿Por qué no hacer return SecondAwait(intermediate);?
Matt Smith
2
Tengo la misma pregunta que Matt, ¿no return SecondAwait(intermediate);alcanzaría el objetivo también en ese caso? Creo que return awaites redundante aquí también ...
TX_
23
@MattSmith Eso no compilaría. Si desea usar awaiten la primera línea, también debe usarla en la segunda.
svick
2
@cateyes No estoy seguro de lo que significa "sobrecarga introducida en paralelo", pero la asyncversión usará menos recursos (hilos) que su versión síncrona.
svick
44
@TomLint Realmente no se compila. Suponiendo que el tipo de retorno de SecondAwaites `cadena, el mensaje de error es:" CS4016: Dado que este es un método asíncrono, la expresión de retorno debe ser de tipo 'cadena' en lugar de 'Tarea <cadena>' ".
svick
23

La única razón por la que querría hacerlo es si hay algún otro awaiten el código anterior, o si de alguna manera está manipulando el resultado antes de devolverlo. Otra forma en que eso podría estar sucediendo es a través de un try/catchcambio en la forma en que se manejan las excepciones. Si no está haciendo nada de eso, tiene razón, no hay razón para agregar la sobrecarga de hacer el método async.

Servy
fuente
44
Al igual que con la respuesta de Stephen, no entiendo por qué sería return awaitnecesario (en lugar de simplemente devolver la tarea de invocación de niños) incluso si hay otro espera en el código anterior . ¿Podría por favor dar una explicación?
TX_
10
@TX_ Si desea eliminar async, ¿cómo esperaría la primera tarea? Debe marcar el método como asyncsi quisiera usar cualquier espera. Si el método está marcado como asyncy tiene un awaitcódigo anterior, entonces necesita awaitla segunda operación asíncrona para que sea del tipo apropiado. Si acaba de eliminar, awaitentonces no se compilaría ya que el valor de retorno no sería del tipo adecuado. Como el método es asyncel resultado, siempre se envuelve en una tarea.
Servy
11
@Noseratio Prueba los dos. Las primeras compilaciones. El segundo no. El mensaje de error le dirá el problema. No devolverá el tipo correcto. Cuando en un asyncmétodo no devuelve una tarea, devuelve el resultado de la tarea que luego se ajustará.
Servy
3
@Servy, por supuesto, tienes razón. En el último caso, regresaríamos Task<Type>explícitamente, mientras que asyncdictemos regresar Type(en lo que el compilador se convertiría Task<Type>).
noseratio
3
@Itsik Bueno, claro, asynces solo azúcar sintáctico para conectar explícitamente las continuaciones. No necesita async hacer nada, pero cuando realiza casi cualquier operación asincrónica no trivial es mucho más fácil trabajar con ella. Por ejemplo, el código que proporcionó en realidad no propaga los errores como desea, y hacerlo correctamente en situaciones aún más complejas comienza a ser mucho más difícil. Si bien nunca lo necesita async , las situaciones que describo son donde está agregando valor para usarlo.
Servy
17

Otro caso que puede necesitar para esperar el resultado es este:

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

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

En este caso, GetIFooAsync()debe esperar el resultado de GetFooAsyncporque el tipo de Tes diferente entre los dos métodos y Task<Foo>no se puede asignar directamente Task<IFoo>. Pero si espera el resultado, se convierte en lo Fooque es directamente asignable IFoo. Luego, el método asíncrono simplemente vuelve a empaquetar el resultado dentro Task<IFoo>y fuera de donde vaya.

Andrew Arnott
fuente
1
De acuerdo, esto es realmente molesto: creo que la causa subyacente es que Task<>es invariable.
StuartLC
7

Hacer asincrónico el método "thunk" de otra manera simple crea una máquina de estado asincrónica en la memoria, mientras que la no asincrónica no. Si bien eso a menudo puede apuntar a la gente a usar la versión no asíncrona porque es más eficiente (lo cual es cierto), también significa que en el caso de un bloqueo, no tienes evidencia de que ese método esté involucrado en la "pila de retorno / continuación" lo que a veces hace que sea más difícil entender el bloqueo.

Entonces, sí, cuando el rendimiento no es crítico (y generalmente no lo es), lanzaré asíncrono en todos estos métodos de thunk para que tenga la máquina de estado asíncrona que me ayude a diagnosticar bloqueos más tarde, y también para ayudar a garantizar que si esos Los métodos thunk siempre evolucionan con el tiempo, se asegurarán de devolver las tareas fallidas en lugar de tirar.

Andrew Arnott
fuente
6

Si no va a utilizar return aguarda, puede arruinar el seguimiento de la pila durante la depuración o cuando se imprime en los registros de excepciones.

Cuando devuelve la tarea, el método cumplió su propósito y está fuera de la pila de llamadas. Cuando lo usa, return awaitlo está dejando en la pila de llamadas.

Por ejemplo:

Pila de llamadas cuando se usa esperar: A esperando la tarea de B => B esperando la tarea de C

Pila de llamadas cuando no se usa esperar: A esperando la tarea de C, que B ha devuelto.

haimb
fuente
2

Esto también me confunde y siento que las respuestas anteriores pasaron por alto su pregunta real:

¿Por qué usar return waitit construct cuando puede devolver directamente Task desde la invocación interna DoAnotherThingAsync ()?

Bueno, a veces realmente quieres un Task<SomeType>, pero la mayoría de las veces realmente quieres una instancia SomeType, es decir, el resultado de la tarea.

De tu código:

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

Una persona que no Task<SomeResult>esté familiarizada con la sintaxis (yo, por ejemplo) podría pensar que este método debería devolver a , pero como está marcado con async, significa que su tipo de retorno real es SomeResult. Si solo usa return foo.DoAnotherThingAsync(), estaría devolviendo una Tarea, que no se compilaría. La forma correcta es devolver el resultado de la tarea, por lo que el return await.

heltonbiker
fuente
1
"tipo de retorno real". Eh? async / await no está cambiando los tipos de retorno. En su ejemplo, var task = DoSomethingAsync();le daría una tarea, noT
Shoe
2
@ Zapato No estoy seguro de haber entendido bien la async/awaitcosa. A mi entender Task task = DoSomethingAsync(), mientras Something something = await DoSomethingAsync()ambos trabajan. El primero le da la tarea adecuada, mientras que el segundo, debido a la awaitpalabra clave, le da el resultado de la tarea una vez que se completa. Podría, por ejemplo, tener Task task = DoSomethingAsync(); Something something = await task;.
heltonbiker