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

242

Una y otra vez, veo que dice que usar async- awaitno 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- awaitno 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.

Ms. Corlib
fuente
17
Las tareas de E / S no están vinculadas a la CPU y, por lo tanto, no requieren un subproceso. El punto principal de asíncrono es no bloquear hilos durante tareas vinculadas a IO.
juharr
24
@jdweng: No, para nada. Incluso si creó nuevos hilos , eso es muy diferente de crear un nuevo proceso.
Jon Skeet
8
Si comprende la programación asincrónica basada en la devolución de llamada, entonces comprende cómo await/ asyncfunciona sin crear ningún subproceso.
user253751
66
No hace que una aplicación sea más receptiva, pero lo desalienta de bloquear sus hilos, que es una causa común de aplicaciones que no responden.
Owen
66
@RubberDuck: Sí, puede usar un subproceso del grupo de subprocesos para la continuación. Pero no está comenzando un hilo de la forma en que el OP se imagina aquí: no es como si dijera "Toma este método ordinario, ahora ejecútalo en un hilo separado, ahí, eso es asíncrono". Es mucho más sutil que eso.
Jon Skeet

Respuestas:

299

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");
}

Voy a explícitamente no hablar de lo que se GetSomethingAsyncestá 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í:

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 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_Clickmé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/awaitno 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:

  1. Todo el código previo await, incluida la llamada aGetSomethingAsync
  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 hasta await
  2. Llama al GetSomethingAsyncmétodo, 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 anterior awaitlleva mucho tiempo, la IU aún se congelará. En nuestro ejemplo, no tanto

  3. Lo que hace la awaitpalabra 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.

  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 awaity 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 usarlo async/awaitcorrectamente, 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 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 GetSomethingAsyncgira 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 señal buena es si la clase / interfaz de métodos en cuestión ha llamado SomethingSomethingAsynco BeginSomethinge EndSomethingY hay un IAsyncResultimplicado.

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.

Lasse V. Karlsen
fuente
11
Entonces, es básicamente lo que el OP describió como " Simulación de ejecución paralela mediante la programación de tareas y el cambio entre ellas ", ¿no es así?
Bergi
44
@ Bergi No del todo. La ejecución es realmente paralela: la tarea de E / S asíncrona está en curso y no requiere subprocesos para continuar (esto es algo que se usó mucho antes de que apareciera Windows; MS DOS también usó E / S asíncrona, aunque no lo hizo). tener múltiples subprocesos!). Por supuesto, también 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.
Luaan
3
Es por eso que quería evitar explícitamente hablar demasiado sobre lo que hizo ese método, ya que la pregunta era sobre async / wait específicamente, que no crea sus propios hilos. Obviamente, se pueden usar para esperar a que se completen los hilos.
Lasse V. Karlsen
66
@ LasseV.Karlsen - Estoy ingiriendo tu gran respuesta, pero todavía estoy colgado de un detalle. Entiendo que el controlador de eventos existe, como en el paso 4, que permite que la bomba de mensajes continúe bombeando, pero ¿ cuándo y dónde continúa la ejecución de "lo que toma dos segundos", si no en un hilo separado? Si se ejecutara en el hilo de la interfaz de usuario, entonces bloquearía la bomba de mensajes de todos modos mientras se está ejecutando porque tiene que ejecutarse algún tiempo en el mismo hilo ... [continúa] ...
rory.ap
3
Me gusta tu explicación con la bomba de mensajes. ¿Cómo difiere su explicación cuando no hay una bomba de mensajes como en la aplicación de consola o servidor web? ¿Cómo se logra la reentrada de un método?
Puchacz
95

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.

Stephen Cleary
fuente
Solo para aclararlo ... Entiendo el alto nivel de lo que sucede cuando se usa async-wait. Con respecto a la creación sin subprocesos: ¿no hay subproceso solo en las solicitudes de E / S a dispositivos que, como usted dijo, tienen sus propios procesadores que manejan la solicitud en sí? ¿Podemos suponer que TODAS las solicitudes de E / S se manejan en procesadores independientes, lo que significa usar Task.Run SOLO en acciones vinculadas a la CPU?
Yonatan Nir
@YonatanNir: No se trata solo de procesadores separados; Cualquier tipo de respuesta basada en eventos es naturalmente asíncrona. Task.Runes más apropiado para acciones vinculadas a la CPU , pero también tiene otros usos.
Stephen Cleary
1
Terminé de leer su artículo y todavía hay algo básico que no entiendo ya que no estoy realmente familiarizado con la implementación de nivel inferior del sistema operativo. Recibí lo que escribiste donde escribiste: "La operación de escritura ahora está" en vuelo ". ¿Cuántos hilos la procesan? Ninguna". . Entonces, si no hay subprocesos, ¿cómo se realiza la operación en sí si no está en un subproceso?
Yonatan Nir
66
¡Esta es la pieza que falta en miles de explicaciones! En realidad, hay alguien haciendo el trabajo en segundo plano con operaciones de E / S. ¡No es un hilo sino otro componente de hardware dedicado que hace su trabajo!
the_dark_destructor
2
@PrabuWeerasinghe: el compilador crea una estructura que contiene las variables estatales y locales. Si una espera necesita ceder (es decir, volver a la persona que llama), esa estructura se encuadra y vive en el montón.
Stephen Cleary
87

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 awaitno 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.

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 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.

Eric Lippert
fuente
Otros lenguajes modernos de alto nivel también admiten un comportamiento cooperativo explícito similar (es decir, la función hace algunas cosas, rinde [posiblemente enviando algún valor / objeto a la persona que llama], continúa donde se detuvo cuando se devuelve el control [posiblemente con entrada adicional suministrada] ) Los generadores son bastante grandes en Python, por un lado.
JAB
2
@JAB: por supuesto. Los generadores se denominan "bloques iteradores" en C # y usan la yieldpalabra clave. Tanto los asyncmé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.
Eric Lippert
1
La analogía con el rendimiento es buena: es una multitarea cooperativa dentro de un proceso. (y así evitar los problemas de estabilidad del sistema de la multitarea cooperativa en todo el sistema)
user253751
3
Creo que el concepto de "interrupciones de la CPU" que se usa para IO no es conocer muchos "programadores" de módem, por lo tanto, piensan que un hilo necesita esperar cada bit de IO.
Ian Ringrose
El método @EricLippert Async de WebClient en realidad crea un hilo adicional, vea aquí stackoverflow.com/questions/48366871/…
KevinBui
28

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 awaituna 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 .

cabeza de jardín
fuente
25

awaity asyncusar 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/awaitusan 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 asyncmétodos transformados del compilador en DFA , el trabajo se divide en pasos, cada uno de los cuales termina con una awaitinstrucción.
El awaitinicia 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.

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.

Margaret Bloom
fuente
16

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 awaitsolo 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.

Andrew Savinykh
fuente
3
No es una competencia en primer lugar; es una colaboracion!
Eric Lippert
16

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:

  • 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 al IO , y de hecho, puede estar vinculado a una combinación de ambos.

Algunos ejemplos:

  • si uso el método Write del FileStreamobjeto (que es un Stream), el procesamiento será, digamos, 1% de CPU y 99% de IO.
  • si uso el método Write del NetworkStreamobjeto (que es un Stream), el procesamiento será, digamos, 1% de CPU y 99% de IO.
  • Si uso el método Write del Memorystreamobjeto (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 Streamobjeto, 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á vinculado 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 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.

Simon Mourier
fuente
1
Esta es una respuesta bastante buena. El uso de theadpool para el trabajo enlazado a la CPU es deficiente en el sentido de que los hilos TP deben usarse para descargar las operaciones de E / S. El trabajo vinculado a la CPU debería estar bloqueado con advertencias, por supuesto, y nada impide el uso de múltiples hilos.
davidcarr
3

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.

vaibhav kumar
fuente
Buen resumen, pero creo que debería responder 2 preguntas más para dar una imagen completa: 1. ¿En qué hilo se ejecuta el código esperado? 2. ¿Quién controla / configura el grupo de subprocesos mencionado: el desarrollador o el entorno de tiempo de ejecución?
stojke
1. En este caso, principalmente el código esperado es una operación vinculada a IO que no usaría hilos de CPU. Si se desea utilizar wait para la operación vinculada a la CPU, se podría generar una tarea separada. 2. El subproceso en el grupo de subprocesos es administrado por el planificador de tareas que forma parte del marco TPL.
vaibhav kumar
2

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. Nuestro postFunctionCallMessagepseudo 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 Taskclase. 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). La Taskclase 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 .ContinueWithdesastre. Entonces aquí es donde el compilador te ayuda. Hace todo este encadenamiento y continuación para ti en segundo plano. Cuando dices awaitque 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.

Calmarius
fuente
0

En realidad, las async awaitcadenas 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 awaits. 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.

Steve Fan
fuente
0

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

Blechdose
fuente