Una y otra vez, veo que dice que usar async
- await
no crea ningún hilo 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 mediante la programación de tareas y el cambio entre ellas (hacer un poco de A, un poco de B, un poco de A, etc.)
Así que si async
- await
no ninguno de esos, entonces ¿cómo puede hacer una solicitud de respuesta? 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.
c#
.net
multithreading
asynchronous
async-await
Ms. Corlib
fuente
fuente
await
/async
funciona sin crear ningún subproceso.Respuestas:
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:
Voy a explícitamente no hablar de lo que se
GetSomethingAsync
está volviendo 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í:
Cuando hace clic en el botón en el formulario, la aplicación parecerá congelarse durante unos 2 segundos, mientras esperamos que se complete este método. 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
button1_Click
método anterior. 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 nuevamente, 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 entraré en demasiados detalles, pero es suficiente decir que el método se divide en estas dos cosas:
await
, incluida la llamada aGetSomethingAsync
await
Ilustración:
Reorganizado:
Básicamente, el método se ejecuta así:
await
Llama al
GetSomethingAsync
método, que hace lo suyo, y devuelve algo que completará 2 segundos en el futuroHasta 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 anterior
await
lleva mucho tiempo, la IU aún se congelará. En nuestro ejemplo, no tantoLo que hace la
await
palabra clave, junto con un poco de magia inteligente de compilación, es que básicamente es algo así como "Ok, sabes qué, simplemente voy a regresar del controlador de eventos de clic de botón aquí. Cuando (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.
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.
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 usarloasync/await
correctamente, volverá a bloquear el bucle de mensajesAquí 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 todavía se filtran en el código circundante, y si no lo hacen, generalmente terminará depurando una aplicación que se rompe aleatoriamente, aparentemente sin una buena razón.
Bien, ¿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:
SomethingSomethingAsync
oBeginSomething
eEndSomething
Y hay unIAsyncResult
implicado.Por lo general, estas cosas no usan un hilo debajo del capó.
OK, ¿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.
fuente
await
puede usarse de la manera en que lo describe, pero generalmente no lo es. Solo se programan las devoluciones de llamada (en el grupo de subprocesos): entre la devolución de llamada y la solicitud, no se necesita ningún subproceso.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.
fuente
Task.Run
es más apropiado para acciones vinculadas a la CPU , pero también tiene otros usos.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 se trata de 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 rompiendo su trabajo en pequeños pedazos y ponerlo en la cola de mensajes para que el subproceso de la interfaz de usuario lo procese más tarde. Están haciendo algo para lograr la asincronía, sino queson 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 ancianos, los niños de hoy con su música rap deberíamos abandonar 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 se parece mucho 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.
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 de que el operador aguardara, 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 . Aguardar le permite escribir código que se parece a la devolución y la finalización están sincronizados, sin que realmente estén sincronizados.
fuente
yield
palabra clave. Tanto losasync
métodos como los iteradores en C # son una forma de rutina , que es el término general para una función que sabe cómo suspender su operación actual para reanudarla más tarde. Varios idiomas tienen corutinas o flujos de control de tipo corutina en estos días.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 una investigación de la mía, que finalmente encontró la pieza que falta:
select()
. Específicamente, IO multiplexación, implementado 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 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 usted es
await
una función, está observando un determinado evento (la finalización de esa función) y luego devuelve el control al bucle principal del evento. 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 corutinas .fuente
await
yasync
usar 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 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
usan Tareas 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 (en realidad intercaladas) 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é los
async
métodos transformados del compilador en DFA , el trabajo se divide en pasos, cada uno de los cuales termina con unaawait
instrucción.El
await
inicia su Tarea y le 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.
Se transforma en algo como esto
1 En realidad, un grupo puede tener su política de creación de tareas.
fuente
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.
Usarlo
await
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.
fuente
Así es como veo todo esto, puede que no sea súper técnicamente preciso, pero me ayuda, al menos :).
Básicamente, hay dos tipos de procesamiento (cálculo) que ocurren en una máquina:
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 al IO , y de hecho, puede estar vinculado a una combinación de ambos.
Algunos ejemplos:
FileStream
objeto (que es un Stream), el procesamiento será, digamos, 1% de CPU y 99% de IO.NetworkStream
objeto (que es un Stream), el procesamiento será, digamos, 1% de CPU y 99% de IO.Memorystream
objeto (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
Stream
objeto, 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:
Antes de async / wait, esencialmente teníamos dos soluciones para esto:
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 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 una necesidad ), 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.
fuente
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 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 una 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.
fuente
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:
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?
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:
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. Nuestro
postFunctionCallMessage
pseudo función de ejemplo 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 eso. 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
Task
clase. Representa un trabajo y cuando lo llama.ContinueWith
, 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). LaTask
clase 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 mostrar al principio. De esta manera, 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
.ContinueWith
desastre. Entonces aquí es donde el compilador te ayuda. Hace todo este encadenamiento y continuación para ti en segundo plano. Cuando dicesawait
que dices, dile al compilador que "para aquí, agrega 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.fuente
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 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 asíncronas, aún necesita un ayudante para verificar si la operación se ha completado, por lo tanto, todavía necesitamos una máquina de estado y determinar qué rutina se puede ejecutar de forma asíncrona.fuente
Esto no responde directamente a la pregunta, pero creo que es una información adicional interesante:
Async and await no crea nuevos hilos por sí mismo. PERO dependiendo de dónde use async wait, la parte síncrona ANTES de la waitit puede ejecutarse en un subproceso diferente que la parte sincronizada DESPUÉS de la wait (por ejemplo, ASP.NET y ASP.NET core se comportan de manera diferente).
En las aplicaciones basadas en UI-Thread (WinForms, WPF) estará en el mismo hilo antes y después. Pero cuando usa asíncrono en un subproceso de grupo de subprocesos, el subproceso antes y después de la espera puede no ser el mismo.
Un gran video sobre este tema
fuente