¿Por qué alguna vez 'esperarías' un método y luego interrogarías inmediatamente su valor de retorno?

24

En este artículo de MSDN , se proporciona el siguiente código de ejemplo (ligeramente editado por brevedad):

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    Department department = await db.Departments.FindAsync(id);

    if (department == null)
    {
        return HttpNotFound();
    }

    return View(department);
}

El FindAsyncmétodo recupera un Departmentobjeto por su ID y devuelve a Task<Department>. Luego, el departamento se verifica de inmediato para ver si es nulo. Según tengo entendido, pedir el valor de la Tarea de esta manera bloqueará la ejecución del código hasta que se devuelva el valor del método esperado, haciendo de esta una llamada síncrona.

¿Por qué harías esto? ¿No sería más simple simplemente llamar al método síncrono Find(id), si vas a bloquear inmediatamente de todos modos?

Robert Harvey
fuente
Podría estar relacionado con la implementación. ... else return null;Luego, deberá verificar que el método realmente encontró el departamento que solicitó.
Jeremy Kato
No veo ninguno en una asp.net, pero en una aplicación destop, al hacerlo de esta manera no estás congelando la interfaz de usuario
Rémi
Aquí hay un enlace que explica el concepto de espera del diseñador ... msdn.microsoft.com/en-us/magazine/hh456401.aspx
Jon Raynor
Esperar solo vale la pena pensar con ASP.NET si los interruptores de contacto de subprocesos lo están ralentizando, o si el uso de la memoria de muchas pilas de subprocesos es un problema para usted.
Ian

Respuestas:

24

Según tengo entendido, pedir el valor de la Tarea de esta manera bloqueará la ejecución del código hasta que se devuelva el valor del método esperado, haciendo de esta una llamada síncrona.

No exactamente.

Cuando llama, await db.Departments.FindAsync(id)la tarea se envía y el subproceso actual se devuelve al grupo para que lo utilicen otras operaciones. El flujo de ejecución está bloqueado (como sería independientemente de usarlo departmentinmediatamente después, si entiendo las cosas correctamente), pero el hilo en sí mismo puede ser utilizado libremente por otras cosas mientras espera que la operación se complete fuera de la máquina (y señalado por un evento o puerto de finalización).

Si llamó, d.Departments.Find(id)entonces el hilo se queda allí y espera la respuesta, a pesar de que la mayor parte del procesamiento se realiza en la base de datos.

Está efectivamente liberando recursos de la CPU cuando el disco está vinculado.

Telastyn
fuente
2
Pero pensé que todo lo que awaithice fue firmar en el resto del método como una continuación en el mismo hilo (hay excepciones; algunos métodos asíncronos hacen girar su propio hilo), o iniciar sesión en el asyncmétodo como una continuación en el mismo hilo y permitir el código restante para ejecutar (como puede ver, no tengo muy claro cómo asyncfunciona). Lo que estás describiendo suena más como una forma sofisticada deThread.Sleep(untilReturnValueAvailable)
Robert Harvey
1
@RobertHarvey: lo asigna como una continuación, pero una vez que ha enviado la tarea (y su continuación) para su procesamiento, no queda nada para ejecutar. No se garantiza que esté en el mismo hilo a menos que lo especifique (a través de ConfigureAwaitiirc).
Telastyn
1
Vea mi respuesta ... la continuación se vuelve a ordenar al hilo original por defecto.
Michael Brown
2
Creo que veo lo que me estoy perdiendo aquí. Para que esto brinde algún beneficio, el marco ASP.NET MVC debe ser awaitllamado public async Task<ActionResult> Details(int? id). De lo contrario, la llamada original simplemente se bloqueará, esperando department == nulla resolverse.
Robert Harvey
2
@RobertHarvey Para cuando await ..."regresa" la FindAsyncllamada ha finalizado. Eso es lo que espera. Se llama esperar porque hace que su código espere cosas. (Pero tenga en cuenta que no es lo mismo que hacer que el hilo actual espere cosas)
user253751
17

Realmente odio que ninguno de los ejemplos muestre cómo es posible esperar algunas líneas antes de esperar la tarea. Considera esto.

Foo foo = await getFoo();
Bar bar = await getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(foo, bar);

Este es el tipo de código que los ejemplos fomentan, y tienes razón. Hay poco sentido en esto. Libera el hilo principal para hacer otras cosas, como responder a la entrada de la interfaz de usuario, pero el verdadero poder de async / wait es que puedo seguir haciendo otras cosas fácilmente mientras espero que se complete una tarea potencialmente larga. El código anterior se "bloqueará" y esperará para ejecutar la línea de impresión hasta que obtengamos Foo & Bar. Sin embargo, no hay necesidad de esperar. Podemos procesar eso mientras esperamos.

Task<Foo> foo = getFoo();
Task<Bar> bar = getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(await foo, await bar);

Ahora, con el código reescrito, no nos detenemos y esperamos nuestros valores hasta que tengamos que hacerlo. Siempre estoy atento a este tipo de oportunidades. Ser inteligente sobre cuándo esperamos puede conducir a mejoras significativas en el rendimiento. Tenemos varios núcleos en estos días, también podría usarlos.

RubberDuck
fuente
1
Hm, eso se trata menos de núcleos / hilos y más sobre el uso de llamadas asincrónicas correctamente.
Deduplicador
No te equivocas @Deduplicator. Es por eso que cité "bloque" en mi respuesta. Es difícil hablar de estas cosas sin mencionar hilos, pero si bien se reconoce que puede haber o no múltiples hilos involucrados.
RubberDuck
Le sugiero que tenga cuidado al esperar dentro de otra llamada al método. A veces, el compilador no entiende esto y espera una excepción realmente extraña. Después de todo async / await es solo azúcar de sintaxis, puede verificar con sharplab.io cómo se ve el código generado. Observé esto en varias ocasiones y ahora solo espero una línea por encima de la llamada donde se necesita el resultado ... no necesito esos dolores de cabeza.
desahuciado
"Hay poco sentido en esto". - Tiene mucho sentido en esto. Los casos en que "seguir haciendo otras cosas" es aplicable en el mismo método no son tan comunes; es mucho más común que solo quieras awaity dejes que el hilo haga cosas completamente diferentes.
Sebastian Redl
Cuando dije "hay poco sentido en esto" @SebastianRedl, me refería al caso en el que obviamente podrías ejecutar ambas tareas en paralelo en lugar de correr, esperar, correr, esperar. Esto es mucho más común de lo que parece pensar. Apuesto a que si miras a tu base de código encontrarás oportunidades.
RubberDuck
6

Entonces hay más que sucede detrás de escena aquí. Async / Await es azúcar sintáctico. Primero mire la firma de la función FindAsync. Devuelve una tarea. Ya estás viendo la magia de la palabra clave, desempaqueta esa Tarea en un Departamento.

La función de llamada no se bloquea. Lo que sucede es que la asignación al departamento y todo lo que sigue a la palabra clave en espera se encuadra en un cierre y, para todos los intentos y propósitos, se pasa al método Task.ContinueWith (la función FindAsync se ejecuta automáticamente en un hilo diferente).

Por supuesto, hay más cosas que suceden detrás de escena porque la operación se vuelve a ordenar al hilo original (por lo que ya no tiene que preocuparse por sincronizar con la interfaz de usuario al realizar una operación en segundo plano) y en el caso de que la función de llamada sea Async ( y ser llamado asincrónicamente) lo mismo sucede en la pila.

Entonces, lo que sucede es que obtienes la magia de las operaciones Async, sin las trampas.

Michael Brown
fuente
1

No, no regresará de inmediato. La espera hace que el método llame asíncrono. Cuando se llama a FindAsync, el método Detalles regresa con una tarea que no ha finalizado. Cuando FindAsync finalice, devolverá su resultado a la variable de departamento y reanudará el resto del método Detalles.

Steve
fuente
No, no hay bloqueo en absoluto. Nunca llegará al departamento == nulo hasta que el método asincrónico ya haya finalizado.
Steve
1
async awaitgeneralmente no crea nuevos subprocesos, e incluso si lo hiciera, aún debe esperar a que el valor del departamento descubra si es nulo.
Robert Harvey
2
Entonces, básicamente, lo que estás diciendo es que, para que esto sea de algún beneficio, la llamada a public async Task<ActionResult> también debe ser awaiteditada.
Robert Harvey
2
@RobertHarvey Sí, tienes la idea. Async / Await es esencialmente viral. Si "espera" una función, también debe esperar cualquier función que la llame. awaitno debe mezclarse con .Wait()o .Result, ya que esto puede causar puntos muertos. La cadena asíncrona / espera generalmente termina en una función con una async voidfirma, que se usa principalmente para controladores de eventos o para funciones invocadas directamente por elementos de la interfaz de usuario.
KChaloux
1
@Steve - " ... entonces estaría en su propio hilo " . No, leer Las tareas aún no son hilos y el asíncrono no es paralelo . Esto incluye la salida real que demuestra que no se crea un nuevo hilo.
ToolmakerSteve
1

Me gusta pensar que "async" es un contrato, un contrato que dice "puedo ejecutar esto de forma asincrónica si lo necesitas, pero también puedes llamarme como cualquier otra función síncrona".

Es decir, un desarrollador estaba haciendo funciones y algunas decisiones de diseño los llevaron a hacer / marcar un montón de funciones como "asíncronas". La persona que llama / consumidor de las funciones tiene la libertad de usarlas según lo decida. Como dice, puede llamar en espera justo antes de la llamada a la función y esperarla, de esta manera la ha tratado como una función síncrona, pero si lo desea, puede llamarla sin esperar como

Task<Department> deptTask = db.Departments.FindAsync(id);

y después, digamos, 10 líneas abajo de la función que llamas

Department d = await deptTask;

por lo tanto, lo trata como una función asincrónica.

Tu decides.

usuario734028
fuente
1
Es más como "Me reservo el derecho de manejar el mensaje de forma asincrónica, utilícelo para obtener el resultado".
Deduplicador
-1

"si vas a bloquear inmediatamente de todos modos", la respuesta es "Sí". Solo cuando necesite una respuesta rápida, esperar / async tiene sentido. Por ejemplo, el subproceso de la interfaz de usuario llega a un método realmente asíncrono, el subproceso de la interfaz de usuario regresará y continuará escuchando el clic del botón, mientras que el siguiente código "espera" será ejecutado por otro subproceso y finalmente obtendrá el resultado.

usuario10200952
fuente
1
¿Qué proporciona esta respuesta que las otras respuestas no?