programacion ejemplo asincrona c# .net asynchronous

ejemplo - programacion asincrona c#



Confusión con respecto a los subprocesos y si los métodos asíncronos son realmente asincrónicos en C# (7)

Estaba leyendo en async / Task.Yield y cuando Task.Yield podría ser útil y encontré esta publicación. Tengo una pregunta con respecto al siguiente de esa publicación:

Cuando utiliza async / await, no hay garantía de que el método que llama cuando espera a FooAsync() se ejecutará de manera asíncrona. La implementación interna es gratuita para regresar usando una ruta completamente sincrónica.

Esto es un poco confuso para mí, probablemente porque la definición de asincrónico en mi cabeza no se está alineando.

En mi opinión, dado que hago principalmente UI dev, el código asíncrono es un código que no se ejecuta en el hilo de UI, sino en otro hilo. Supongo que en el texto que cité, un método no es verdaderamente asincrónico si bloquea en cualquier hilo (incluso si es un hilo del grupo de subprocesos, por ejemplo).

Pregunta:

Si tengo una tarea de larga ejecución que está unida a la CPU (digamos que está haciendo un montón de cálculos difíciles), entonces ejecutar esa tarea de forma asincrónica debe estar bloqueando algún hilo ¿no? Algo tiene que hacer realmente las matemáticas. Si lo espero, algunos hilos se bloquean.

¿Qué es un ejemplo de un método realmente asíncrono y cómo funcionarían realmente? ¿Están limitados a operaciones de E / S que aprovechan algunas capacidades de hardware para que no se bloquee ningún hilo?


Cuando utiliza async / await, no hay garantía de que el método que llama cuando espera a FooAsync () se ejecutará de manera asíncrona. La implementación interna es gratuita para regresar usando una ruta completamente sincrónica.

Esto es un poco confuso para mí, probablemente porque la definición de asincrónico en mi cabeza no se está alineando.

Esto simplemente significa que hay dos casos al llamar a un método asíncrono.

La primera es que, al devolverte la tarea, la operación ya está completa; esta sería una ruta sincrónica. El segundo es que la operación todavía está en progreso: esta es la ruta asíncrona.

Considere este código, que debería mostrar estas dos rutas. Si la clave está en un caché, se devuelve sincrónicamente. De lo contrario, se inicia una operación asíncrona que llama a una base de datos:

Task<T> GetCachedDataAsync(string key) { if(cache.TryGetvalue(key, out T value)) { return Task.FromResult(value); // synchronous: no awaits here. } // start a fully async op. return GetDataImpl(); async Task<T> GetDataImpl() { value = await database.GetValueAsync(key); cache[key] = value; return value; } }

Entonces, entendiendo eso, se puede deducir que en teoría es la llamada de la database.GetValueAsync() de database.GetValueAsync() puede tener un código similar y ser capaz de regresar síncrono: así que incluso su ruta asíncrona puede terminar ejecutándose 100% sincrónicamente. Pero su código no necesita preocuparse: async / await maneja ambos casos sin problemas.

Si tengo una tarea de larga ejecución que está unida a la CPU (digamos que está haciendo un montón de cálculos difíciles), entonces ejecutar esa tarea de forma asincrónica debe estar bloqueando algún hilo ¿no? Algo tiene que hacer realmente las matemáticas. Si lo espero, algunos hilos se bloquean.

El bloqueo es un término bien definido: significa que su hilo ha cedido su ventana de ejecución mientras espera algo (E / S, exclusión mutua, etc.). Por lo tanto, su hilo que hace las matemáticas no se considera bloqueado: en realidad está realizando un trabajo.

¿Qué es un ejemplo de un método realmente asíncrono y cómo funcionarían realmente? ¿Están limitados a operaciones de E / S que aprovechan algunas capacidades de hardware para que no se bloquee ningún hilo?

Un "método realmente asíncrono" sería uno que simplemente nunca bloquea. Por lo general, termina involucrando E / S, pero también puede significar await su pesado código matemático cuando desee su hilo actual para otra cosa (como en el desarrollo de la interfaz de usuario) o cuando intente introducir el paralelismo:

async Task<double> DoSomethingAsync() { double x = await ReadXFromFile(); Task<double> a = LongMathCodeA(x); Task<double> b = LongMathCodeB(x); await Task.WhenAll(a, b); return a.Result + b.Result; }


Estaba leyendo en async / await

¿Puedo recomendar mi introducción async ?

y cuando Task.Yield podría ser útil

Casi nunca. Encuentro que de vez en cuando es útil al hacer pruebas unitarias.

En mi opinión, dado que hago principalmente UI dev, el código asíncrono es un código que no se ejecuta en el hilo de UI, sino en otro hilo.

El código asíncrono puede ser sin hilos .

Supongo que en el texto que cité, un método no es verdaderamente asincrónico si bloquea en cualquier hilo (incluso si es un hilo del grupo de subprocesos, por ejemplo).

Yo diría que eso es correcto. Utilizo el término "verdaderamente asíncrono" para las operaciones que no bloquean ningún subproceso (y que no son síncronos). También utilizo el término "asincrónica falsa" para las operaciones que parecen asíncronas, pero solo funcionan de esa manera porque se ejecutan o bloquean un hilo del grupo de subprocesos.

Si tengo una tarea de larga ejecución que está unida a la CPU (digamos que está haciendo un montón de cálculos difíciles), entonces ejecutar esa tarea de forma asincrónica debe estar bloqueando algún hilo ¿no? Algo tiene que hacer realmente las matemáticas.

Sí; en este caso, querrá definir ese trabajo con una API síncrona (ya que es un trabajo sincrónico), y luego puede llamarlo desde su subproceso de interfaz de usuario utilizando Task.Run , por ejemplo:

var result = await Task.Run(() => MySynchronousCpuBoundCode());

Si lo espero, algunos hilos se bloquean.

No; el subproceso de la agrupación de subprocesos se usaría para ejecutar el código (no realmente bloqueado), y el subproceso de la interfaz de usuario está esperando asincrónicamente que se complete ese código (también no está bloqueado).

¿Qué es un ejemplo de un método realmente asíncrono y cómo funcionarían realmente?

NetworkStream.WriteAsync (indirectamente) le pide a la tarjeta de red que escriba algunos bytes. No hay ningún hilo responsable de escribir los bytes uno a la vez y esperar a que se escriba cada byte. La tarjeta de red maneja todo eso. Cuando la tarjeta de red termina de escribir todos los bytes, (eventualmente) completa la tarea devuelta desde WriteAsync .

¿Están limitados a operaciones de E / S que aprovechan algunas capacidades de hardware para que no se bloquee ningún hilo?

No del todo, aunque las operaciones de E / S son los ejemplos fáciles. Otro ejemplo bastante fácil son los temporizadores (por ejemplo, Task.Delay ). Aunque puedes construir una API verdaderamente asíncrona alrededor de cualquier tipo de "evento".


Esto es un poco confuso para mí, probablemente porque la definición de asincrónico en mi cabeza no se está alineando.

Bien por pedir una aclaración.

En mi opinión, dado que hago principalmente UI dev, el código asíncrono es un código que no se ejecuta en el hilo de UI, sino en otro hilo.

Esa creencia es común pero falsa. No es necesario que el código asincrónico se ejecute en ningún segundo subproceso.

Imagina que estás cocinando el desayuno. Pon una tostada en la tostadora, y mientras esperas a que salte la tostada, revisas tu correo de ayer, pagas algunas cuentas, y bueno, la tostada apareció. Terminas de pagar esa factura y luego vas a poner mantequilla a tu tostada.

¿Dónde contrataste a un segundo trabajador para que vigilara tu tostadora?

No lo hiciste Los hilos son trabajadores. Los flujos de trabajo asincrónicos pueden suceder todos en un hilo. El objetivo del flujo de trabajo asincrónico es evitar contratar más trabajadores si es posible evitarlo.

Si tengo una tarea de larga ejecución que está unida a la CPU (digamos que está haciendo un montón de cálculos difíciles), entonces ejecutar esa tarea de forma asincrónica debe estar bloqueando algún hilo ¿no? Algo tiene que hacer realmente las matemáticas.

Aquí, te daré un problema difícil de resolver. Aquí hay una columna de 100 números; por favor agrégalos a mano. Entonces agregas el primero al segundo y haces un total. Luego, agrega el total acumulado al tercero y obtiene un total. Entonces, oh, demonios, falta la segunda página de números. Recuerda dónde estabas e intenta hacer un brindis. Oh, mientras la tostada estaba brindando, llegó una carta con los números restantes. Cuando termine de untar el pan tostado, continúe sumando esos números, y recuerde comer el pan tostado la próxima vez que tenga un momento libre.

¿Dónde está la parte donde contrató a otro trabajador para agregar los números? El trabajo computacionalmente costoso no necesita ser sincrónico, y no necesita bloquear un hilo . Lo que hace que el trabajo computacional sea potencialmente asincrónico es la capacidad de detenerlo, recordar dónde estaba, hacer otra cosa, recordar qué hacer después y reanudar donde lo dejó.

Ahora es posible contratar a un segundo trabajador que no hace más que agregar números, y luego es despedido. Y podría preguntarle a ese trabajador "¿Ya terminaste?" y si la respuesta es no, podrías hacer un sándwich hasta que terminen. De esa forma, tanto usted como el trabajador están ocupados. Pero no es un requisito que la asincronía involucre a múltiples trabajadores.

Si lo espero, algunos hilos se bloquean.

NO NO NO. Esta es la parte más importante de tu malentendido. await no significa "comenzar este trabajo de forma asíncrona". await significa que "aquí tengo un resultado producido asincrónicamente que podría no estar disponible. Si no está disponible, busca otro trabajo para hacer en este hilo para que no estemos bloqueando el hilo. Esperar es lo opuesto a lo que acabas de decir.

¿Qué es un ejemplo de un método realmente asíncrono y cómo funcionarían realmente? ¿Están limitados a operaciones de E / S que aprovechan algunas capacidades de hardware para que no se bloquee ningún hilo?

El trabajo asincrónico a menudo implica hardware personalizado o múltiples subprocesos, pero no es necesario.

No pienses en los trabajadores . Piense en los flujos de trabajo . La esencia de la asincronía es dividir los flujos de trabajo en pequeñas partes para que pueda determinar el orden en que esas partes deben suceder , y luego ejecutar cada parte sucesivamente , pero permitiendo que las partes que no tienen dependencias entre sí se intercalen .

En un flujo de trabajo asíncrono, puede detectar fácilmente lugares en el flujo de trabajo donde se expresa una dependencia entre las partes . Tales partes están marcadas con await . Ese es el significado de await : el código que sigue depende de que se complete esta parte del flujo de trabajo, por lo que si no se completa, busque otra tarea que hacer y vuelva más tarde cuando la tarea esté completa. El objetivo es mantener al trabajador trabajando, incluso en un mundo donde los resultados necesarios se producen en el futuro.


Asincrónico no implica Paralelo

Asíncrono solo implica concurrencia. De hecho, incluso el uso de subprocesos explícitos no garantiza que se ejecutarán simultáneamente (por ejemplo, cuando los subprocesos tienen afinidad por el mismo núcleo único, o más comúnmente cuando en primer lugar hay solo un núcleo en la máquina).

Por lo tanto, no debe esperar que una operación asincrónica ocurra simultáneamente con otra cosa. Asíncrono solo significa que sucederá, eventualmente en otro momento ( a (griego) = sin, sin (griego) = juntos, khronos (griego) = tiempo. => Asíncrono = no ocurre al mismo tiempo).

Nota: La idea de la asincronía es que en la invocación no le importa cuándo se ejecutará realmente el código. Esto permite que el sistema aproveche el paralelismo, si es posible, para ejecutar la operación. Incluso puede ejecutarse inmediatamente. Incluso podría suceder en el mismo hilo ... más sobre eso más adelante.

Cuando await la operación asíncrona, estás creando concurrencia ( com (latin) = together, currere (latin) = run. => "Concurrent" = para ejecutar juntos ). Eso es porque está pidiendo que la operación asincrónica llegue a completarse antes de continuar. Podemos decir que la ejecución converge. Esto es similar al concepto de unir hilos.

Cuando asincrónico no puede ser Paralelo

Cuando utiliza async / await, no hay garantía de que el método que llama cuando espera a FooAsync () se ejecutará de manera asíncrona. La implementación interna es gratuita para regresar usando una ruta completamente sincrónica.

Esto puede suceder de tres maneras:

  1. Es posible usar await en cualquier cosa que devuelva Task . Cuando reciba la Task ya podría haberse completado.

    Sin embargo, eso solo no implica que haya sido sincronizado. De hecho, sugiere que se ejecutó de forma asíncrona y finalizó antes de obtener la instancia de la Task .

    Tenga en cuenta que puede await en una tarea ya completada:

    private static async Task CallFooAsync() { await FooAsync(); } private static Task FooAsync() { return Task.CompletedTask; } private static void Main() { CallFooAsync().Wait(); }

    Además, si un método async no ha await , se ejecutará sincrónicamente.

    Nota: Como ya sabe, un método que devuelve una Task puede estar esperando en la red, o en el sistema de archivos, etc. ... hacerlo no implica iniciar un nuevo Thread o poner algo en el ThreadPool .

  2. En un contexto de sincronización manejado por un único hilo, el resultado será ejecutar la Task sincrónica, con algunos gastos generales. Este es el caso del hilo de UI, hablaré más sobre lo que sucede a continuación.

  3. Es posible escribir un TaskScheduler personalizado para ejecutar siempre las tareas sincrónicamente. En el mismo hilo, eso hace la invocación.

    Nota: Recientemente escribí un SyncrhonizationContext personalizado que ejecuta tareas en un solo hilo. Puede encontrarlo en Crear un planificador de tareas (System.Threading.Tasks) . TaskScheduler como resultado TaskScheduler con una llamada a FromCurrentSynchronizationContext .

    El TaskScheduler predeterminado TaskScheduler las invocaciones al ThreadPool . Sin embargo, cuando aguarde la operación, si no se ha ejecutado en el ThreadPool , intentará eliminarlo del ThreadPool y ejecutarlo en línea (en el mismo subproceso que está esperando ... el subproceso está esperando de todos modos, por lo que no es ocupado).

    Nota: Una excepción notable es una Task marcada con LongRunning . LongRunning Task s se ejecutará en un hilo separado .

Tu pregunta

Si tengo una tarea de larga ejecución que está unida a la CPU (digamos que está haciendo un montón de cálculos difíciles), entonces ejecutar esa tarea de forma asincrónica debe estar bloqueando algún hilo ¿no? Algo tiene que hacer realmente las matemáticas. Si lo espero, algunos hilos se bloquean.

Si está haciendo cálculos, deben ocurrir en algún hilo, esa parte es correcta.

Sin embargo, la belleza de async y await es que el hilo de espera no tiene que ser bloqueado (más sobre esto más adelante). Sin embargo, es muy fácil dispararse en el pie al tener la tarea esperada programada para ejecutarse en el mismo subproceso que está esperando, lo que resulta en la ejecución sincrónica (que es un error fácil en el hilo de la interfaz de usuario).

Una de las características clave de async y await es que toman el SynchronizationContext de la persona que llama. Para la mayoría de los hilos que resulta en el uso del TaskScheduler defecto (que, como se mencionó anteriormente, usa ThreasPool ). Sin embargo, para el hilo de UI, significa publicar las tareas en la cola de mensajes, esto significa que se ejecutarán en el hilo de la interfaz de usuario. La ventaja de esto es que no tiene que usar Invoke o BeginInvoke para acceder a los componentes de la interfaz de usuario.

Antes de entrar en cómo await una Task desde el hilo de la interfaz de usuario sin bloquearlo, quiero señalar que es posible implementar un TaskScheduler donde, si await en una Task , no bloqueas el hilo ni lo dejas inactivo, en su lugar, permite que su hilo elija otra Task que está esperando su ejecución. Cuando estaba colaborando con Tasks para .NET 2.0 , experimenté con esto.

¿Qué es un ejemplo de un método realmente asíncrono y cómo funcionarían realmente? ¿Están limitados a operaciones de E / S que aprovechan algunas capacidades de hardware para que no se bloquee ningún hilo?

Parece confundir asincrónicamente con no bloquear un hilo . Si lo que desea es un ejemplo de operaciones asincrónicas en .NET que no requiera bloquear un hilo, una forma de hacerlo que le resultará fácil de comprender es usar continuations lugar de await . Y para las continuaciones que necesita ejecutar en el subproceso de interfaz de usuario, puede usar FromCurrentSynchronizationContext .

No implemente una espera de centrifugado elegante . Y con eso me refiero a usar un Timer , Application.Idle o algo por el estilo.

Cuando utilizas la función async le estás diciendo al compilador que reescriba el código del método de una manera que permita su ruptura. El resultado es similar a las continuaciones, con una sintaxis mucho más conveniente. Cuando el hilo llega a la await , se programará la Task , y el hilo puede continuar libremente después de la invocación async actual (fuera del método). Cuando la Task está lista, la continuación (después de la await ) está programada.

Para el hilo de la interfaz de usuario esto significa que una vez que llega el momento, es gratis continuar procesando los mensajes. Una vez que finalice la Task esperada, se programará la continuación (después de la await ). Como resultado, llegar a await no implica bloquear el hilo.

Sin embargo, agregar ciegamente async y await no solucionará todos sus problemas.

Te presento un experimento. Obtenga una nueva aplicación de Windows Forms, coloque un Button y un TextBox , y agregue el siguiente código:

private async void button1_Click(object sender, EventArgs e) { await WorkAsync(5000); textBox1.Text = @"DONE"; } private async Task WorkAsync(int milliseconds) { Thread.Sleep(milliseconds); }

Bloquea la IU. Lo que ocurre es que, como se mencionó anteriormente, await usa automáticamente el SynchronizationContext del hilo llamante. En este caso, ese es el hilo de UI. Por lo tanto, WorkAsync se ejecutará en el subproceso de interfaz de usuario.

Esto es lo que pasa:

  • Los subprocesos de UI obtienen el mensaje de clic y llaman al controlador de evento click
  • En el controlador de eventos click, el subproceso de interfaz de usuario llega a await WorkAsync(5000)
  • WorkAsync(5000) (y la programación de su continuación) está programado para ejecutarse en el contexto de sincronización actual, que es el contexto de sincronización de subprocesos de interfaz de usuario ... lo que significa que publica un mensaje para ejecutarlo
  • El hilo de UI ahora es libre de procesar más mensajes
  • El subproceso de la interfaz de usuario selecciona el mensaje para ejecutar WorkAsync(5000) y programar su continuación
  • El hilo de la interfaz de usuario llama a WorkAsync(5000) con la continuación
  • En WorkAsync , el subproceso de la interfaz de usuario ejecuta Thread.Sleep . La IU ahora no responde durante 5 segundos.
  • La continuación programa el resto del controlador de eventos click para ejecutar, esto se hace mediante la publicación de otro mensaje para el hilo de la interfaz de usuario
  • El hilo de UI ahora es libre de procesar más mensajes
  • El subproceso de la interfaz de usuario selecciona el mensaje para continuar en el controlador de evento click
  • El hilo de la interfaz de usuario actualiza el cuadro de texto

El resultado es una ejecución sincrónica, con gastos generales.

Sí, debe usar Task.Delay en Task.Delay lugar. Ese no es el punto; considere Sleep una posición para algunos cálculos. El punto es que el simple hecho de usar async y await todas partes no le dará una aplicación que sea automáticamente paralela. Es mucho mejor elegir qué quieres ejecutar en un hilo de fondo (por ejemplo, en el ThreadPool ) y qué quieres ejecutar en el hilo de UI.

Ahora prueba el siguiente código:

private async void button1_Click(object sender, EventArgs e) { await Task.Run(() => Work(5000)); textBox1.Text = @"DONE"; } private void Work(int milliseconds) { Thread.Sleep(milliseconds); }

Encontrarás que esperar no bloquea la interfaz de usuario. Esto se debe a que en este caso Thread.Sleep se está ejecutando en ThreadPool gracias a Task.Run . Y gracias a que button1_Click es async , una vez que el código llega, el hilo de la interfaz de usuario está libre para seguir funcionando. Una vez realizada la Task , el código se reanudará después de await gracias a que el compilador reescribió el método para permitir precisamente eso.

Esto es lo que pasa:

  • Los subprocesos de UI obtienen el mensaje de clic y llaman al controlador de evento click
  • En el controlador de eventos click, el subproceso de interfaz de usuario llega a await Task.Run(() => Work(5000))
  • Task.Run(() => Work(5000)) (y la programación de su continuación) está programado para ejecutarse en el contexto de sincronización actual, que es el contexto de sincronización de subprocesos de UI ... lo que significa que publica un mensaje para ejecutarlo
  • El hilo de UI ahora es libre de procesar más mensajes
  • El subproceso de la interfaz de usuario selecciona el mensaje para ejecutar Task.Run(() => Work(5000)) y programar su continuación cuando termine
  • El subproceso UI llama a Task.Run(() => Work(5000)) con continuación, esto se ejecutará en ThreadPool
  • El hilo de UI ahora es libre de procesar más mensajes

Cuando el ThreadPool finaliza, la continuación programará el resto del controlador de eventos click para que se ejecute, esto se hace mediante la publicación de otro mensaje para el hilo de UI. Cuando el subproceso de la interfaz de usuario selecciona el mensaje para continuar en el controlador de eventos de clic, se actualizará el cuadro de texto.


Este tema es bastante amplio y pueden surgir varias discusiones. Sin embargo, usar async y await en C # se considera programación asincrónica. Sin embargo, cómo funciona la asincronía es una discusión totalmente diferente. Hasta .NET 4.5 no había asincronización y esperaban palabras clave, y los desarrolladores tenían que desarrollarse directamente en contra de la Biblioteca paralela de tareas (TPL). Allí el desarrollador tenía control total sobre cuándo y cómo crear nuevas tareas e incluso hilos. Sin embargo, esto tenía una desventaja ya que, al no ser realmente un experto en este tema, las aplicaciones podrían sufrir graves problemas de rendimiento y errores debido a las condiciones de carrera entre los hilos y demás.

A partir de .NET 4.5, se introdujeron las palabras clave async y await, con un nuevo enfoque para la programación asincrónica. Las palabras clave async y await no provocan la creación de subprocesos adicionales. Los métodos asíncronos no requieren multithreading porque un método async no se ejecuta en su propio hilo. El método se ejecuta en el contexto de sincronización actual y usa el tiempo en la secuencia solo cuando el método está activo. Puede usar Task.Run para mover el trabajo vinculado a CPU a un hilo de fondo, pero un hilo de fondo no ayuda con un proceso que está esperando que los resultados estén disponibles.

El enfoque asincrónico de la programación asincrónica es preferible a los enfoques existentes en casi todos los casos. En particular, este enfoque es mejor que BackgroundWorker para operaciones vinculadas a IO porque el código es más simple y no tiene que protegerse contra las condiciones de carrera. Puede leer más sobre este tema HERE .

No me considero un cinturón negro de C # y algunos desarrolladores más experimentados pueden plantear nuevas discusiones, pero como principio espero haber podido responder a su pregunta.


La respuesta de Stephen ya es genial, así que no voy a repetir lo que dijo; He hecho mi parte justa repitiendo los mismos argumentos muchas veces en (y en otros lugares).

En cambio, permítanme centrarme en una cosa abstracta importante sobre el código asíncrono: no es un calificador absoluto. No tiene sentido decir que un fragmento de código es asincrónico; siempre es asincrónico con respecto a otra cosa . Esto es bastante importante

El propósito de await es crear flujos de trabajo sincrónicos además de las operaciones asíncronas y algunos de conexión de código síncrono. Su código aparece perfectamente sincrónico 1 con el código mismo.

var a = await A(); await B(a);

El orden de los eventos está especificado por las invocaciones de await . B usa el valor de retorno de A, lo que significa que A debe haberse ejecutado antes de B. El método que contiene este código tiene un flujo de trabajo síncrono, y los dos métodos A y B son sincrónicos entre sí.

Esto es muy útil, porque los flujos de trabajo síncronos suelen ser más fáciles de pensar y, lo que es más importante, muchos flujos de trabajo simplemente son sincrónicos. Si B necesita el resultado de A para ejecutarse, debe ejecutarse después de A 2 . Si necesita realizar una solicitud HTTP para obtener la URL para otra solicitud HTTP, debe esperar a que se complete la primera solicitud; no tiene nada que ver con la programación de subprocesos / tareas. Tal vez podríamos llamar a esto "sincronicidad inherente", aparte de "sincronicidad accidental" donde forzar el orden en cosas que no necesitan ordenarse.

Tu dices:

En mi opinión, dado que hago principalmente UI dev, el código asíncrono es un código que no se ejecuta en el hilo de UI, sino en otro hilo.

Está describiendo código que se ejecuta de forma asíncrona con respecto a la interfaz de usuario. Ese es sin duda un caso muy útil para la asincronía (a la gente no le gusta la interfaz de usuario que deja de responder). Pero se trata simplemente de un caso específico de un principio más general: permitir que las cosas sucedan fuera de lugar unas con respecto a otras. Una vez más, no es un absoluto: desea que ocurran algunos eventos fuera de servicio (por ejemplo, cuando el usuario arrastra la ventana o la barra de progreso cambia, la ventana aún debe volver a dibujarse), mientras que otros no deben estar desordenados (el botón Procesar no se debe hacer clic antes de que termine la acción Cargar). await en este caso de uso no es tan diferente de usar Application.DoEvents en principio - presenta muchos de los mismos problemas y beneficios.

Esta es también la parte donde la cita original se vuelve interesante. La IU necesita un hilo para ser actualizado. Ese hilo invoca un controlador de eventos, que puede estar usando await . ¿Significa que la línea donde se está await permitirá que la interfaz de usuario se actualice a sí misma en respuesta a la entrada del usuario? No.

Primero, necesita comprender que await usa su argumento, como si fuera una llamada a un método. En mi ejemplo, A debe haber sido invocado antes de que el código generado por await pueda hacer algo, incluso "liberando el control nuevamente al bucle de UI". El valor de retorno de A es Task<T> lugar de solo T , que representa un "valor posible en el futuro" - y await - el código generado verifica para ver si el valor ya está allí (en cuyo caso simplemente continúa en el mismo hilo ) o no (lo que significa que tenemos que volver a lanzar el hilo al bucle de UI). Pero en cualquier caso, el valor Task<T>debe haber sido devuelto por A

Considere esta implementación:

public async Task<int> A() { Thread.Sleep(1000); return 42; }

La persona que llama necesita A para devolver un valor (una tarea de int); ya que no hay await en el método, eso significa el return 42; . Pero eso no puede suceder antes de que el sueño termine, porque las dos operaciones son sincrónicas con respecto al hilo. El hilo de la persona que llama se bloqueará por un segundo, independientemente de si usa await o no; el bloqueo está en A() sí mismo, no await theTaskResultOfA .

Por el contrario, considere esto:

public async Task<int> A() { await Task.Delay(1000); return 42; }

Tan pronto como la ejecución llega a la await , ve que la tarea que se está esperando no ha terminado todavía y devuelve el control a su llamador; y la await en la persona que llama devuelve el control a su interlocutor. Hemos logrado hacer que parte del código sea asíncrono con respecto a la interfaz de usuario. La sincronicidad entre el subproceso de interfaz de usuario y A fue accidental, y lo eliminamos.

The important part here is: there''s no way to distinguish between the two implementations from the outside without inspecting the code. Only the return type is part of the method signature - it doesn''t say the method will execute asynchronously, only that it may . This may be for any number of good reasons, so there''s no point in fighting it - for example, there''s no point in breaking the thread of execution when the result is already available:

var responseTask = GetAsync("http://www.google.com"); // Do some CPU intensive task ComputeAllTheFuzz(); response = await responseTask;

We need to do some work. Some events can run asynchronously with respect to others (in this case, ComputeAllTheFuzz is independent of the HTTP request) and are asynchronous. But at some point, we need to get back to a synchronous workflow (for example, something that requires both the result of ComputeAllTheFuzz and the HTTP request). That''s the await point, which synchronizes the execution again (if you had multiple asynchronous workflows, you''d use something like Task.WhenAll ). However, if the HTTP request managed to complete before the computation, there''s no point in releasing control at the await point - we can simply continue on the same thread. There''s been no waste of the CPU - no blocking of the thread; it does useful CPU work. But we didn''t give any opportunity for the UI to update.

This is of course why this pattern is usually avoided in more general asynchronous methods. It is useful for some uses of asynchronous code (avoiding wasting threads and CPU time), but not others (keeping the UI responsive). If you expect such a method to keep the UI responsive, you''re not going to be happy with the result. But if you use it as part of a web service, for example, it will work great - the focus there is on avoiding wasting threads, not keeping the UI responsive (that''s already provided by asynchronously invoking the service endpoint - there''s no benefit from doing the same thing again on the service side).

In short, await allows you to write code that is asynchronous with respect to its caller. It doesn''t invoke a magical power of asynchronicity, it isn''t asynchronous with respect to everything, it doesn''t prevent you from using the CPU or blocking threads. It just gives you the tools to easily make a synchronous workflow out of asynchronous operations, and present part of the whole workflow as asynchronous with respect to its caller.

Let''s consider an UI event handler. If the individual asynchronous operations happen to not need a thread to execute (eg asynchronous I/O), part of the asynchronous method may allow other code to execute on the original thread (and the UI stays responsive in those parts). When the operation needs the CPU/thread again, it may or may not require the original thread to continue the work. If it does, the UI will be blocked again for the duration of the CPU work; if it doesn''t (the awaiter specifies this using ConfigureAwait(false) ), the UI code will run in parallel. Assuming there''s enough resources to handle both, of course. If you need the UI to stay responsive at all times, you cannot use the UI thread for any execution long enough to be noticeable - even if that means you have to wrap an unreliable "usually asynchronous, but sometimes blocks for a few seconds" async method in a Task.Run . There''s costs and benefits to both approaches - it''s a trade-off, as with all engineering :)

  1. Of course, perfect as far as the abstraction holds - every abstraction leaks, and there''s plenty of leaks in await and other approaches to asynchronous execution.
  2. A sufficiently smart optimizer might allow some part of B to run, up to the point where the return value of A is actually needed; this is what your CPU does with normal "synchronous" code ( Out of order execution ). Such optimizations must preserve the appearance of synchronicity, though - if the CPU misjudges the ordering of operations, it must discard the results and present a correct ordering.

Here''s asynchronous code which shows how async / await allows code to block and release control to another flow, then resume control but not needing a thread.

public static async Task<string> Foo() { Console.WriteLine("In Foo"); await Task.Yield(); Console.WriteLine("I''m Back"); return "Foo"; } static void Main(string[] args) { var t = new Task(async () => { Console.WriteLine("Start"); var f = Foo(); Console.WriteLine("After Foo"); var r = await f; Console.WriteLine(r); }); t.RunSynchronously(); Console.ReadLine(); }

So it''s that releasing of control and resynching when you want results that''s key with async/await ( which works well with threading )

NOTE: No Threads were blocked in the making of this code :)

I think sometimes the confusion might come from "Tasks" which doesn''t mean something running on its own thread. It just means a thing to do, async / await allows tasks to be broken up into stages and coordinate those various stages into a flow.

It''s kind of like cooking, you follow the recipe. You need to do all the prep work before assembling the dish for cooking. So you turn on the oven, start cutting things, grating things, etc. Then you await the temp of oven and await the prep work. You could do it by yourself swapping between tasks in a way that seems logical (tasks / async / await), but you can get someone else to help grate cheese while you chop carrots (threads) to get things done faster.