understanding run method ejemplos create await async c# .net multithreading asynchronous async-await

method - task.run async c#



Si async-await no crea ningún subproceso adicional, ¿cómo hace que las aplicaciones respondan? (10)

Una y otra vez, veo que dice que usar async - await no crea ningún subproceso adicional. Eso no tiene sentido porque las únicas formas en que una computadora puede hacer más de una cosa a la vez es

  • Realmente haciendo más de 1 cosa a la vez (ejecución en paralelo, utilizando múltiples procesadores)
  • Simulando al programar tareas y cambiar entre ellas (hacer un poco de A, un poco de B, un poco de A, etc.)

Entonces, si async - await no lo hace, ¿cómo puede hacer que una aplicación responda? Si solo hay 1 subproceso, llamar a cualquier método significa esperar a que el método se complete antes de hacer cualquier otra cosa, y los métodos dentro de ese método tienen que esperar el resultado antes de continuar, y así sucesivamente.


las únicas formas en que una computadora puede parecer estar haciendo más de 1 cosa a la vez es (1) Realmente haciendo más de 1 cosa a la vez, (2) simulando mediante la programación de tareas y el cambio entre ellas. Entonces, si async-waitit ninguno de esos

No es que esperar no hace ninguno de esos. Recuerde, el propósito de await no es hacer que el código síncrono sea mágicamente asíncrono . Es para permitir el uso de las mismas técnicas que usamos para escribir código síncrono cuando llamamos a código asíncrono . Esperar consiste en hacer que el código que usa operaciones de alta latencia se vea como un código que usa operaciones de baja latencia . Esas operaciones de alta latencia pueden estar en subprocesos, pueden estar en hardware de propósito especial, pueden estar desgarrando su trabajo en pequeños pedazos y colocarlo en la cola de mensajes para su posterior procesamiento por el subproceso de la interfaz de usuario. Están haciendo algo para lograr la asincronía, pero ellos son los que lo están haciendo. Await simplemente te permite aprovechar esa asincronía.

Además, creo que te falta una tercera opción. Nosotros, los viejos, los niños de hoy con su música rap deberíamos salir de mi césped, etc., recordamos el mundo de Windows a principios de la década de 1990. No había máquinas con múltiples CPU ni programadores de subprocesos. Querías ejecutar dos aplicaciones de Windows al mismo tiempo, tenías que ceder . La multitarea fue cooperativa . El sistema operativo le dice a un proceso que puede ejecutarse, y si se comporta mal, priva a todos los demás procesos de ser atendidos. Se ejecuta hasta que cede, y de alguna manera tiene que saber cómo retomar donde lo dejó la próxima vez que el sistema operativo le devuelva el control . El código asincrónico de subproceso único es muy parecido a eso, con "esperar" en lugar de "ceder". Esperar significa "Voy a recordar dónde lo dejé aquí y dejaré que alguien más corra por un tiempo; llámame cuando la tarea que estoy esperando esté completa y continuaré donde lo dejé". Creo que puedes ver cómo eso hace que las aplicaciones sean más receptivas, tal como lo hizo en los 3 días de Windows.

llamar a cualquier método significa esperar a que se complete el método

Ahí está la clave que te estás perdiendo. Un método puede regresar antes de que se complete su trabajo . Esa es la esencia de la asincronía allí mismo. Un método devuelve, devuelve una tarea que significa "este trabajo está en progreso; dígame qué hacer cuando esté completo". El trabajo del método no se realiza, aunque haya regresado .

Antes del operador aguardando, tenía que escribir un código que pareciera espagueti atravesado con queso suizo para lidiar con el hecho de que tenemos trabajo que hacer después de la finalización, pero con la devolución y la finalización desincronizadas . Await le permite escribir código que se parece al retorno y la finalización se sincronizan, sin que en realidad se sincronicen.


En realidad, las async await cadenas son máquinas de estado generadas por el compilador CLR.

async await sin embargo, usa hilos que TPL está usando un grupo de hilos para ejecutar tareas.

La razón por la que la aplicación no está bloqueada es porque la máquina de estados puede decidir qué co-rutina ejecutar, repetir, verificar y decide de nuevo.

Otras lecturas:

¿Qué genera async & await?

Async Await and the Generated StateMachine

Asíncrono C # y F # (III.): ¿Cómo funciona? - Tomás Petricek

Editar :

Bueno. Parece que mi elaboración es incorrecta. Sin embargo, debo señalar que las máquinas de estado son activos importantes para el async await s. Incluso si acepta E / S asincrónicas, aún necesita un ayudante para verificar si la operación está completa, por lo tanto, todavía necesitamos una máquina de estado y determinar qué rutina se puede ejecutar de forma asíncrona.


Así es como veo todo esto, puede que no sea súper técnicamente preciso, pero al menos me ayuda :).

Básicamente, hay dos tipos de procesamiento (cálculo) que ocurren en una máquina:

  • procesamiento que sucede en la CPU
  • procesamiento que ocurre en otros procesadores (GPU, tarjeta de red, etc.), llamémoslos IO.

Entonces, cuando escribimos un fragmento de código fuente, después de la compilación, dependiendo del objeto que usemos (y esto es muy importante), el procesamiento estará vinculado a la CPU o IO , y de hecho, puede estar vinculado a una combinación de ambos.

Algunos ejemplos:

  • si utilizo el método Write del objeto FileStream (que es un Stream), el procesamiento será, digamos, 1% de CPU y 99% de IO.
  • si utilizo el método Write del objeto NetworkStream (que es un Stream), el procesamiento será, digamos, 1% de CPU y 99% de IO.
  • Si uso el método Write del objeto Memorystream (que es un Stream), el procesamiento estará 100% vinculado a la CPU.

Entonces, como puede ver, desde el punto de vista de un programador orientado a objetos, aunque siempre estoy accediendo a un objeto Stream , lo que sucede debajo puede depender en gran medida del tipo final del objeto.

Ahora, para optimizar las cosas, a veces es útil poder ejecutar código en paralelo (tenga en cuenta que no uso la palabra asincrónica) si es posible y / o necesario.

Algunos ejemplos:

  • En una aplicación de escritorio, quiero imprimir un documento, pero no quiero esperarlo.
  • Mi servidor web sirve a muchos clientes al mismo tiempo, cada uno con sus páginas en paralelo (no serializadas).

Antes de async / wait, esencialmente teníamos dos soluciones para esto:

  • Hilos . Era relativamente fácil de usar, con las clases Thread y ThreadPool. Los subprocesos están vinculados a la CPU solamente .
  • El "viejo" modelo de programación asíncrona Begin / End / AsyncCallback . Es solo un modelo, no le dice si estará vinculado a la CPU o IO. Si echa un vistazo a las clases Socket o FileStream, está enlazado a IO, lo cual es genial, pero rara vez lo usamos.

El asíncrono / espera es solo un modelo de programación común, basado en el concepto de Tarea . Es un poco más fácil de usar que los subprocesos o grupos de subprocesos para tareas vinculadas a la CPU, y mucho más fácil de usar que el antiguo modelo Begin / End. Sin embargo, Undercovers es "solo" un envoltorio súper sofisticado con funciones completas para ambos.

Por lo tanto, la verdadera ganancia se debe principalmente a las tareas IO Bound , tarea que no usa la CPU, pero async / wait todavía es solo un modelo de programación, no le ayuda a determinar cómo / dónde se realizará el procesamiento al final.

Significa que no es porque una clase tiene un método "DoSomethingAsync" que devuelve un objeto Task que se puede suponer que estará vinculado a la CPU (lo que significa que puede ser bastante inútil , especialmente si no tiene un parámetro de token de cancelación), o IO Bound (lo que significa que probablemente sea obligatorio ), o una combinación de ambos (dado que el modelo es bastante viral, la unión y los beneficios potenciales pueden ser, al final, súper mixtos y no tan obvios).

Entonces, volviendo a mis ejemplos, hacer mis operaciones de escritura usando async / await en MemoryStream permanecerá vinculado a la CPU (probablemente no me beneficiaré de ello), aunque seguramente me beneficiaré con archivos y transmisiones de red.


En realidad, async / waitit no es tan mágico. El tema completo es bastante amplio, pero para una respuesta rápida pero lo suficientemente completa a su pregunta, creo que podemos manejarlo.

Abordemos un simple evento de clic de botón en una aplicación de formularios Windows Forms:

public async void button1_Click(object sender, EventArgs e) { Console.WriteLine("before awaiting"); await GetSomethingAsync(); Console.WriteLine("after awaiting"); }

No voy a hablar explícitamente sobre lo que sea que GetSomethingAsync está regresando por ahora. Digamos que esto es algo que se completará después de, digamos, 2 segundos.

En un mundo tradicional, no asíncrono, el controlador de eventos de clic de botón se vería así:

public void button1_Click(object sender, EventArgs e) { Console.WriteLine("before waiting"); DoSomethingThatTakes2Seconds(); Console.WriteLine("after waiting"); }

Cuando hace clic en el botón en el formulario, la aplicación parecerá congelarse por alrededor de 2 segundos, mientras esperamos que este método se complete. Lo que sucede es que la "bomba de mensajes", básicamente un bucle, está bloqueada.

Este bucle continuamente pregunta a Windows "¿Alguien ha hecho algo, como mover el mouse, hacer clic en algo? ¿Necesito volver a pintar algo? Si es así, ¡dígamelo!" y luego procesa ese "algo". Este bucle recibió un mensaje en el que el usuario hizo clic en "botón1" (o el tipo de mensaje equivalente de Windows) y terminó llamando a nuestro método button1_Click arriba. Hasta que este método regrese, este ciclo ahora está atascado en espera. Esto lleva 2 segundos y durante esto, no se procesan mensajes.

La mayoría de las cosas que se ocupan de las ventanas se realizan mediante mensajes, lo que significa que si el bucle de mensajes deja de enviar mensajes, incluso por un segundo, el usuario lo nota rápidamente. Por ejemplo, si mueve el bloc de notas o cualquier otro programa encima de su propio programa, y ​​luego lo aleja de nuevo, se envía una ráfaga de mensajes de pintura a su programa que indica qué región de la ventana que ahora de repente se volvió a ver. Si el bucle de mensajes que procesa estos mensajes está esperando algo bloqueado, entonces no se realiza ninguna pintura.

Entonces, si en el primer ejemplo, async/await no crea nuevos hilos, ¿cómo lo hace?

Bueno, lo que sucede es que tu método se divide en dos. Este es uno de esos temas generales, por lo que no voy a entrar en demasiados detalles, pero es suficiente decir que el método se divide en estas dos cosas:

  1. Todo el código previo a la await , incluida la llamada a GetSomethingAsync
  2. Todo el código siguiente await

Ilustración:

code... code... code... await X(); ... code... code... code...

Reorganizado:

code... code... code... var x = X(); await X; code... code... code... ^ ^ ^ ^ +---- portion 1 -------------------+ +---- portion 2 ------+

Básicamente, el método se ejecuta así:

  1. Ejecuta todo para await
  2. Llama al método GetSomethingAsync , que hace lo suyo, y devuelve algo que completará 2 segundos en el futuro

    Hasta ahora todavía estamos dentro de la llamada original a button1_Click, sucediendo en el hilo principal, llamado desde el bucle de mensajes. Si el código que lleva a await toma mucho tiempo, la IU aún se congelará. En nuestro ejemplo, no tanto

  3. Lo que la palabra clave await , junto con un poco de magia de compilación inteligente, es básicamente algo así como "Ok, sabes qué, simplemente voy a regresar del controlador de eventos de clic de botón aquí. Cuando tú (como en, lo que "estoy esperando" para completar, avíseme porque todavía me queda algo de código para ejecutar ".

    En realidad, le permitirá a la clase SynchronizationContext saber que está hecho, lo que, dependiendo del contexto de sincronización real que esté en juego en este momento, se pondrá en cola para su ejecución. La clase de contexto utilizada en un programa de Windows Forms lo pondrá en cola usando la cola que está bombeando el bucle de mensajes.

  4. Por lo tanto, vuelve al bucle de mensajes, que ahora es libre de continuar enviando mensajes, como mover la ventana, cambiar su tamaño o hacer clic en otros botones.

    Para el usuario, la interfaz de usuario ahora responde de nuevo, procesa otros clics en los botones, cambia el tamaño y, lo que es más importante, vuelve a dibujar , por lo que no parece congelarse.

  5. 2 segundos después, lo que estamos esperando se completa y lo que sucede ahora es que (bueno, el contexto de sincronización) coloca un mensaje en la cola que está mirando el bucle de mensajes, diciendo "Oye, tengo más código para ejecutar ", y este código es todo el código después de la espera.
  6. Cuando el bucle de mensajes llega a ese mensaje, básicamente "volverá a ingresar" ese método donde lo dejó, justo después de await y continuar ejecutando el resto del método. Tenga en cuenta que este código se llama nuevamente desde el bucle de mensajes, por lo que si este código hace algo largo sin usar async/await correctamente, volverá a bloquear el bucle de mensajes

Aquí hay muchas partes móviles debajo del capó, así que aquí hay algunos enlaces a más información, iba a decir "si lo necesita", pero este tema es bastante amplio y es bastante importante conocer algunas de esas partes móviles . Invariablemente, comprenderá que async / await sigue siendo un concepto con fugas. Algunas de las limitaciones y problemas subyacentes aún se filtran en el código circundante, y si no lo hacen, generalmente terminará depurando una aplicación que se rompe al azar por aparentemente no hay una buena razón.

OK, ¿y si GetSomethingAsync gira un hilo que se completará en 2 segundos? Sí, entonces obviamente hay un nuevo hilo en juego. Sin embargo, este subproceso no se debe a la asincronía de este método, sino a que el programador de este método eligió un subproceso para implementar código asincrónico. Casi todas las E / S asíncronas no usan un hilo, usan cosas diferentes. async/await por sí mismos no generan nuevos hilos, pero obviamente las "cosas que esperamos" pueden implementarse usando hilos.

Hay muchas cosas en .NET que no necesariamente hacen girar un subproceso por sí solas, pero siguen siendo asíncronas:

  • Solicitudes web (y muchas otras cosas relacionadas con la red que llevan tiempo)
  • Lectura y escritura asíncrona de archivos
  • y muchos más, una buena señal es si la clase / interfaz en cuestión tiene métodos llamados SomethingSomethingAsync o BeginSomething y EndSomething y hay un IAsyncResult involucrado.

Por lo general, estas cosas no usan un hilo debajo del capó.

OK, ¿entonces quieres algo de ese "tema general"?

Bueno, preguntémosle a Try Roslyn sobre nuestro clic de botón:

Prueba Roslyn

No voy a vincular en la clase completa generada aquí, pero es bastante sangriento.


Estoy realmente contento de que alguien haya hecho esta pregunta, porque durante mucho tiempo también creí que los hilos eran necesarios para la concurrencia. Cuando vi por primera vez bucles de eventos , pensé que eran una mentira. Pensé para mí mismo "no hay forma de que este código pueda ser concurrente si se ejecuta en un solo hilo". Tenga en cuenta que esto es después de que ya había pasado por la lucha de comprender la diferencia entre concurrencia y paralelismo.

Después de investigar por mi cuenta, finalmente encontré la pieza que faltaba: select() . Específicamente, multiplexación de E / S, implementada por varios núcleos con diferentes nombres: select() , poll() , epoll() , kqueue() . Estas son llamadas al sistema que, si bien los detalles de implementación difieren, le permiten pasar un conjunto de descriptores de archivo para ver. Luego, puede realizar otra llamada que se bloquee hasta que cambie uno de los descriptores de archivos observados.

Por lo tanto, uno puede esperar en un conjunto de eventos IO (el bucle de eventos principal), manejar el primer evento que se completa y luego devolver el control al bucle de eventos. Enjuague y repita.

¿Como funciona esto? Bueno, la respuesta corta es que se trata de magia a nivel de kernel y hardware. Hay muchos componentes en una computadora además de la CPU, y estos componentes pueden funcionar en paralelo. El núcleo puede controlar estos dispositivos y comunicarse directamente con ellos para recibir ciertas señales.

Estas llamadas al sistema de multiplexación de E / S son el componente fundamental de los bucles de eventos de un solo subproceso como node.js o Tornado. Cuando await una función, está observando un determinado evento (la finalización de esa función) y luego devuelve el control al bucle de eventos principal. Cuando finaliza el evento que está viendo, la función (eventualmente) comienza desde donde la dejó. Las funciones que le permiten suspender y reanudar la computación como esta se llaman coroutines .


Lo explico en su totalidad en mi blog No hay hilo .

En resumen, los sistemas modernos de E / S hacen un uso intensivo de DMA (Acceso directo a memoria). Hay procesadores especiales y dedicados en tarjetas de red, tarjetas de video, controladores HDD, puertos serie / paralelo, etc. Estos procesadores tienen acceso directo al bus de memoria y manejan la lectura / escritura de manera completamente independiente de la CPU. La CPU solo necesita notificar al dispositivo la ubicación en la memoria que contiene los datos, y luego puede hacer lo suyo hasta que el dispositivo genere una interrupción notificando a la CPU que la lectura / escritura se ha completado.

Una vez que la operación está en vuelo, no hay trabajo para la CPU y, por lo tanto, no hay subproceso.


No voy a competir con Eric Lippert o Lasse V. Karlsen, y otros, solo me gustaría llamar la atención sobre otra faceta de esta pregunta, que creo que no se mencionó explícitamente.

El uso de await por sí solo no hace que su aplicación responda mágicamente. Si hace lo que haga en el método que está esperando desde los bloques de subprocesos de la interfaz de usuario, seguirá bloqueando su interfaz de usuario de la misma manera que lo haría la versión no esperable .

Debe escribir su método de espera específicamente para que genere un nuevo subproceso o use algo como un puerto de finalización (que devolverá la ejecución en el subproceso actual y llamará a otra cosa para continuar cada vez que se indique el puerto de finalización). Pero esta parte está bien explicada en otras respuestas.


Resumiendo otras respuestas:

Async / await se crea principalmente para tareas vinculadas a IO, ya que al usarlas, se puede evitar bloquear el hilo de llamada. Su uso principal es con subprocesos de la interfaz de usuario donde no se desea que el subproceso se bloquee en una operación vinculada a E / S.

Async no crea su propio hilo. El subproceso del método de llamada se utiliza para ejecutar el método asíncrono hasta que encuentre un modo de espera. El mismo hilo continúa ejecutando el resto del método de llamada más allá de la llamada al método asíncrono. Dentro del método asincrónico llamado, después de regresar de lo esperado, la continuación se puede ejecutar en un subproceso del grupo de subprocesos, el único lugar en el que aparece un subproceso separado.


Trato de explicarlo de abajo hacia arriba. Quizás alguien lo encuentre útil. Estuve allí, hice eso, lo reinventé, cuando hice juegos simples en DOS en Pascal (buenos viejos tiempos ...)

Entonces ... En una aplicación impulsada por cada evento tiene un bucle de eventos dentro que es algo como esto:

while (getMessage(out message)) // pseudo-code { dispatchMessage(message); // pseudo-code }

Los marcos generalmente te ocultan este detalle, pero está ahí. La función getMessage lee el próximo evento de la cola de eventos o espera hasta que ocurra un evento: movimiento del mouse, pulsación de tecla, tecla, clic, etc. Y luego dispatchMessage distribuye el evento al controlador de eventos apropiado. Luego espera el próximo evento y así sucesivamente hasta que llegue un evento de cierre que salga de los bucles y finalice la aplicación.

Los controladores de eventos deben ejecutarse rápidamente para que el bucle de eventos pueda sondear más eventos y la interfaz de usuario siga respondiendo. ¿Qué sucede si un clic en un botón desencadena una operación costosa como esta?

void expensiveOperation() { for (int i = 0; i < 1000; i++) { Thread.Sleep(10); } }

Bueno, la interfaz de usuario deja de responder hasta que finaliza la operación de 10 segundos mientras el control permanece dentro de la función. Para resolver este problema, debe dividir la tarea en partes pequeñas que puedan ejecutarse rápidamente. Esto significa que no puede manejar todo en un solo evento. Debe hacer una pequeña parte del trabajo, luego publicar otro evento en la cola de eventos para solicitar la continuación.

Entonces cambiarías esto a:

void expensiveOperation() { doIteration(0); } void doIteration(int i) { if (i >= 1000) return; Thread.Sleep(10); // Do a piece of work. postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. }

En este caso, solo se ejecuta la primera iteración, luego publica un mensaje en la cola de eventos para ejecutar la siguiente iteración y regresa. En nuestro ejemplo, la pseudo función postFunctionCallMessage pone un evento "llamar a esta función" en la cola, por lo que el despachador de eventos lo llamará cuando llegue. Esto permite que se procesen todos los demás eventos de la GUI mientras se ejecutan continuamente piezas de un trabajo de larga duración.

Mientras esta tarea de larga ejecución se esté ejecutando, su evento de continuación siempre estará en la cola de eventos. Así que básicamente inventaste tu propio programador de tareas. Donde los eventos de continuación en la cola son "procesos" que se están ejecutando. En realidad, esto es lo que hacen los sistemas operativos, excepto que el envío de los eventos de continuación y el regreso al bucle del planificador se realiza a través de la interrupción del temporizador de la CPU donde el sistema operativo registró el código de cambio de contexto, por lo que no necesita preocuparse por ello. Pero aquí está escribiendo su propio planificador, por lo que debe preocuparse por eso, hasta ahora.

Por lo tanto, podemos ejecutar tareas de larga duración en un solo hilo paralelo a la GUI dividiéndolas en pequeños fragmentos y enviando eventos de continuación. Esta es la idea general de la clase Task . Representa una pieza de trabajo y cuando llama .ContinueWith ella, define qué función llamar como la siguiente pieza cuando finaliza la pieza actual (y su valor de retorno se pasa a la continuación). La clase Task utiliza un grupo de subprocesos, donde hay un bucle de eventos en cada subproceso que espera hacer trabajos similares a los que yo quería al principio. De esta forma, puede tener millones de tareas ejecutándose en paralelo, pero solo unos pocos hilos para ejecutarlas. Pero funcionaría igual de bien con un solo hilo, siempre y cuando sus tareas se dividan adecuadamente en pequeños trozos, cada uno de ellos parece ejecutarse en paralelo.

Pero hacer todo este encadenamiento dividiendo el trabajo en pequeños trozos manualmente es un trabajo engorroso y arruina totalmente el diseño de la lógica, porque todo el código de la tarea en segundo plano básicamente es un desastre .ContinueWith . Entonces aquí es donde el compilador te ayuda. Hace todo este encadenamiento y continuación para ti en segundo plano. Cuando diga que await le dice, diga al compilador que "pare aquí, agregue el resto de la función como una tarea de continuación". El compilador se encarga del resto, por lo que no tiene que hacerlo.


await y async use tareas, no hilos.

El marco tiene un grupo de subprocesos listos para ejecutar algún trabajo en forma de objetos de tarea ; enviar una tarea al grupo significa seleccionar un subproceso 1 libre, ya existente , para llamar al método de acción de la tarea.
Crear una tarea es crear un nuevo objeto, mucho más rápido que crear un nuevo hilo.

Dada una Tarea es posible adjuntarle una Continuación , es un nuevo objeto de Tarea que se ejecutará una vez que finalice el hilo.

Como async/await usa Task s, no crean un nuevo hilo.

Si bien las técnicas de programación de interrupciones se usan ampliamente en todos los sistemas operativos modernos, no creo que sean relevantes aquí.
Puede tener dos tareas vinculadas a la CPU ejecutándose en paralelo (intercaladas en realidad) en una sola CPU usando aysnc/await .
Eso no podría explicarse simplemente con el hecho de que el sistema operativo admite IORP en cola.

La última vez que revisé el compilador transformó los métodos async en DFA , el trabajo se divide en pasos, cada uno de los cuales termina con una instrucción de await .
La await inicia su Tarea y adjunta una continuación para ejecutar el siguiente paso.

Como ejemplo conceptual, aquí hay un ejemplo de pseudocódigo.
Las cosas se están simplificando en aras de la claridad y porque no recuerdo todos los detalles exactamente.

method: instr1 instr2 await task1 instr3 instr4 await task2 instr5 return value

Se transforma en algo como esto

int state = 0; Task nextStep() { switch (state) { case 0: instr1; instr2; state = 1; task1.addContinuation(nextStep()); task1.start(); return task1; case 1: instr3; instr4; state = 2; task2.addContinuation(nextStep()); task2.start(); return task2; case 2: instr5; state = 0; task3 = new Task(); task3.setResult(value); task3.setCompleted(); return task3; } } method: nextStep();

1 En realidad, un grupo puede tener su política de creación de tareas.