run method español ejemplos create await async c# multithreading asynchronous

c# - method - ¿Por qué las aplicaciones web se están volviendo locas con await/async hoy en día?



task c# (3)

Vengo de un back end / cliente grueso, así que tal vez me falta algo ... pero recientemente miré la fuente de un servidor de tokens JWT de código abierto y los autores se volvieron locos con await / async. Como en cada método y cada línea.

Entiendo para qué sirve el patrón ... ejecutar tareas de ejecución larga en un hilo separado. En mis días de cliente mayor, lo usaría si un método pudiera tomar unos segundos, para no bloquear el hilo de la GUI ... pero definitivamente no en un método que lleva unos pocos ms.

¿Es este uso excesivo de await / async algo que necesitas para el desarrollo web o para algo como Angular? Esto estaba en un servidor de token JWT, por lo que ni siquiera se ve lo que tiene que ver con ninguno de esos. Es solo un punto final REST.

¿Cómo mejorará el rendimiento de cada línea asincrónica? Para mí, va a matar el rendimiento de girar todos esos hilos, ¿no?


Entiendo para qué sirve el patrón ... ejecutar tareas de ejecución larga en un hilo separado.

Esto no es para lo que es este patrón .

Await no pone la operación en un nuevo hilo. Asegúrate de que sea muy claro para ti. Aguardar programa el trabajo restante como la continuación de la operación de alta latencia.

Await no convierte una operación sincrónica en una operación concurrente asincrónica. Await permite a los programadores que trabajan con un modelo que ya es asíncrono escribir su lógica para parecerse a flujos de trabajo síncronos . No esperes ni crea ni destruye la asincronía; administra la asincronía existente.

Alternar un nuevo hilo es como contratar a un trabajador. Cuando esperas una tarea, no contratas a un trabajador para que realice esa tarea. Usted está preguntando "¿esta tarea ya está hecha? Si no, llámeme cuando esté lista para continuar haciendo un trabajo que depende de esa tarea. Mientras tanto, voy a trabajar en esta otra cosa aquí. "

Si está haciendo sus impuestos y encuentra que necesita un número de su trabajo, y el correo aún no ha llegado, no contrata a un trabajador para esperar en el buzón . Anote dónde estuvo en sus impuestos, haga otras cosas, y cuando llegue el correo, retome donde lo dejó. Eso está esperando . Está esperando asíncronamente un resultado .

¿Es este uso excesivo de await / async algo que necesitas para el desarrollo web o para algo como Angular?

Es para gestionar la latencia.

¿Cómo va a hacer que cada línea asíncrona mejore el rendimiento?

En dos maneras. En primer lugar, al garantizar que las aplicaciones sigan siendo receptivas en un mundo con operaciones de alta latencia. Ese tipo de rendimiento es importante para los usuarios que no quieren que sus aplicaciones se cuelguen. En segundo lugar, al proporcionar a los desarrolladores herramientas para expresar las relaciones de dependencia de datos en flujos de trabajo asincrónicos. Al no bloquear operaciones de alta latencia, los recursos del sistema se liberan para trabajar en operaciones desbloqueadas.

Para mí, va a matar el rendimiento de girar todos esos hilos, ¿no?

No hay hilos. La concurrencia es un mecanismo para lograr la asincronía; no es el único.

Ok, entonces si escribo código como: espera algún Método1 (); espera algúnMethod2 (); espera unMétodo3 (); que mágicamente va a hacer que la aplicación sea más receptiva?

Más receptivo en comparación con qué? ¿Comparado con llamar a esos métodos sin esperarlos? No claro que no. ¿Comparado con esperar sincrónicamente para que las tareas se completen? Absolutamente sí.

Eso es lo que no entiendo, supongo. Si aguardaste en los 3 al final, entonces sí, estás ejecutando los 3 métodos en paralelo.

No no no. Deja de pensar en el paralelismo. No es necesario que haya ningún paralelismo.

Piensa en ello de esta manera. Desea hacer un sándwich de huevo frito. Usted tiene las siguientes tareas:

  • Freír un huevo
  • Tostar un poco de pan
  • Montar un sándwich

Tres tareas. La tercera tarea depende de los resultados de las dos primeras, pero las primeras dos tareas no dependen una de la otra. Entonces, aquí hay algunos flujos de trabajo:

  • Pon un huevo en la sartén. Mientras el huevo se está friendo, mira el huevo.
  • Una vez que el huevo esté listo, ponga un poco de tostada en la tostadora. Mire la tostadora.
  • Una vez que termine la tostada, ponga el huevo sobre la tostada.

El problema es que podrías poner la tostada en la tostadora mientras el huevo se está cocinando. Flujo de trabajo alternativo:

  • Pon un huevo en la sartén. Configura una alarma que suena cuando el huevo está hecho.
  • Ponga tostadas en la tostadora. Establezca una alarma que suene cuando termine la tostada.
  • Revisa tu correo. Haz tus impuestos Polaca los cubiertos. Lo que sea que necesites hacer
  • Cuando hayan sonado las dos alarmas, tome el huevo y la tostada, agréguelos y tendrá un sándwich.

¿Ves por qué el flujo de trabajo asíncrono es mucho más eficiente? Obtendrá muchas cosas mientras espera que se complete la operación de alta latencia. Pero no contrataste a un chef de huevo y a un chef tostado . ¡No hay nuevos hilos!

El flujo de trabajo que propuse sería:

eggtask = FryEggAsync(); toasttask = MakeToastAsync(); egg = await eggtask; toast = await toasttask; return MakeSandwich(egg, toast);

Ahora, compara eso con:

eggtask = FryEggAsync(); egg = await eggtask; toasttask = MakeToastAsync(); toast = await toasttask; return MakeSandwich(egg, toast);

¿Ves cómo difiere ese flujo de trabajo? Este flujo de trabajo es:

  • Pon un huevo en la sartén y activa una alarma.
  • Haga otro trabajo hasta que la alarma se apague.
  • Saca el huevo de la sartén; pon el pan en la tostadora. Configurar una alarma...
  • Haga otro trabajo hasta que la alarma se apague.
  • Cuando la alarma suene, ensamble el sándwich.

Este flujo de trabajo es menos eficiente porque no hemos logrado capturar el hecho de que las tareas de pan tostado y huevo son de alta latencia e independientes . Pero seguramente es un uso más eficiente de los recursos que no hacer nada mientras esperas que el huevo se cocine.

El objetivo de todo esto es que los hilos son increíblemente caros, así que no hagas nuevos hilos. Más bien, haga un uso más eficiente del hilo que tiene al ponerlo a trabajar mientras realiza operaciones de alta latencia . Aguardar no se trata de hacer girar nuevos hilos; se trata de hacer más trabajo en un hilo en un mundo con alta computación de latencia.

Tal vez ese cálculo se está haciendo en otro hilo, tal vez está bloqueado en el disco, lo que sea. No importa El punto es, aguardar es para administrar esa asincronía, no crearla .

Estoy teniendo dificultades para entender cómo la programación asincrónica puede ser posible sin usar el paralelismo en alguna parte. Al igual que, ¿cómo le dices al programa que comience con la tostada mientras esperas los huevos sin DoEggs () corriendo concurrentemente, al menos internamente?

Regrese a la analogía. Estás haciendo un emparedado de huevo, los huevos y las tostadas se están cocinando, y entonces comienzas a leer tu correo. Cuando se terminan los huevos, se llega a la mitad del correo, por lo que deja el correo a un lado y saca el huevo del fuego. Luego vuelves al correo. Luego, la tostada termina y haces el sándwich. Luego terminas de leer tu correo después de hacer el sándwich. ¿Cómo hiciste todo eso sin contratar personal, una persona para leer el correo, una persona para cocinar el huevo, una para hacer la tostada y otra para armar el sándwich? Lo hiciste todo con un solo trabajador.

¿Cómo hiciste eso? Al dividir las tareas en pequeños trozos, observar qué piezas se tienen que hacer y en qué orden, y luego realizar varias tareas de forma cooperativa .

Los niños de hoy con sus grandes modelos de memoria virtual planos y procesos multiproceso piensan que así es como siempre ha sido, pero mi memoria se remonta a los días de Windows 3, que no tenía nada de eso. Si quería que ocurrieran dos cosas "en paralelo", eso es lo que hizo: dividir las tareas en partes pequeñas y se turnaron para ejecutar las partes. Todo el sistema operativo se basó en este concepto.

Ahora, podría ver la analogía y decir "OK, pero parte del trabajo, como tostar realmente el pan tostado, lo está haciendo una máquina", y esa es la fuente del paralelismo. Claro, no tuve que contratar a un trabajador para brindar por el pan, pero logré el paralelismo en el hardware. Y esa es la manera correcta de pensarlo. El paralelismo de hardware y el paralelismo de hilos son diferentes . Cuando realiza una solicitud asíncrona al subsistema de red para buscar un registro de una base de datos, no hay un hilo que esté esperando el resultado. El hardware logra el paralelismo en un nivel muy, muy inferior al de los hilos del sistema operativo.

Si desea una explicación más detallada de cómo funciona el hardware con el sistema operativo para lograr la asincronía, lea " No hay hilo " por Stephen Cleary.

Entonces, cuando veas "asincrónico", no pienses "paralelo". Piense que "la operación de alta latencia se divide en pequeños pedazos". Si hay muchas operaciones de este tipo, cuyas piezas no dependen entre sí, entonces puede intercalar cooperativamente la ejecución de esas piezas en un hilo.

Como se puede imaginar, es muy difícil escribir flujos de control donde puede abandonar lo que está haciendo en este momento, hacer otra cosa y retomar sin interrupciones donde lo dejó. ¡Es por eso que hacemos que el compilador haga ese trabajo! El punto de "esperar" es que le permita administrar esos flujos de trabajo asincrónicos describiéndolos como flujos de trabajo síncronos. Donde quiera que haya un punto en el que pueda dejar esta tarea a un lado y volver a ella más tarde, escriba "esperar". El compilador se encargará de convertir su código en muchas piezas pequeñas que pueden programarse en un flujo de trabajo asincrónico.

ACTUALIZAR:

En su último ejemplo, ¿cuál sería la diferencia entre

eggtask = FryEggAsync(); egg = await eggtask; toasttask = MakeToastAsync(); toast = await toasttask;

egg = await FryEggAsync(); toast = await MakeToastAsync();?

Supongo que los llama sincrónicamente, pero los ejecuta de forma asíncrona. Debo admitir que nunca antes me había molestado en esperar la tarea por separado.

No hay diferencia.

Cuando se llama a FryEggAsync se FryEggAsync independientemente de si aparece antes o no. await es un operador . Funciona con lo que devuelve la llamada a FryEggAsync . Es como cualquier otro operador.

Permítanme decir esto de nuevo: await es un operador y su operando es una tarea. Es un operador muy inusual, desde luego, pero gramaticalmente es un operador y opera con un valor igual que cualquier otro operador.

Déjame decirlo de nuevo: await no es polvo mágico que pones en un sitio de llamadas y de repente ese sitio de llamadas se remota a otro hilo. La llamada ocurre cuando ocurre la llamada, la llamada devuelve un valor , y ese valor es una referencia a un objeto que es un operando legal para el operador de await .

Entonces sí,

var x = Foo(); var y = await x;

y

var y = await Foo();

son lo mismo, lo mismo que

var x = Foo(); var y = 1 + x;

y

var y = 1 + Foo();

son lo mismo

Así que veamos esto una vez más, porque parece que creen que el mito que await causa asincronía. No es asi.

async Task M() { var eggtask = FryEggAsync();

Supongamos que se llama M() . Se llama FryEggAsync . Sincrónicamente No existe una llamada asincrónica; Si ve una llamada, el control pasa al destinatario hasta que regrese el destinatario. El destinatario devuelve una tarea que representa un huevo que estará disponible en el futuro .

¿Cómo hace FryEggAsync esto? No lo sé y no me importa. Todo lo que sé es que lo llamo, y recupero un objeto que representa un valor futuro. Tal vez ese valor se produce en un hilo diferente. Tal vez se produce en este hilo, pero en el futuro . Tal vez es producido por hardware de propósito especial, como un controlador de disco o una tarjeta de red. No me importa Me importa que regrese una tarea.

egg = await eggtask;

Ahora tomamos esa tarea y await pregunta "¿has terminado?" Si la respuesta es sí, entonces se le da al egg el valor producido por la tarea. Si la respuesta es no, M() devuelve una Task representa "el trabajo de M se completará en el futuro". El resto de M () se registra como la continuación de eggtask , por lo que cuando eggtask completa, llamará a M() nuevamente y lo recogerá no desde el principio , sino desde la asignación hasta el egg . M () es un método reanudable en cualquier punto . El compilador hace la magia necesaria para que eso suceda.

Así que ahora hemos vuelto. El hilo sigue haciendo lo que sea que haga. En algún momento, el huevo está listo, por lo que se invoca la continuación de eggtask , lo que hace que se llame nuevamente a M() . Se reanuda en el punto donde lo dejó: asignando el huevo recién producido al egg . Y ahora seguimos en camiones:

toasttask = MakeToastAsync();

De nuevo, la llamada devuelve una tarea y nosotros:

toast = await toasttask;

verifica si la tarea está completa. Si es así, asignamos toast . Si no, volvemos de M () nuevamente , y la continuación de toasttask es * el resto de M ().

Y así.

La eliminación de las variables de task no hace nada pertinente. Se asigna el almacenamiento para los valores; simplemente no se le dio un nombre.

OTRA ACTUALIZACIÓN:

¿Hay algún caso para llamar a los métodos de devolución de tareas tan pronto como sea posible, pero aguardarlos lo más tarde posible?

El ejemplo dado es algo así como:

var task = FooAsync(); DoSomethingElse(); var foo = await task; ...

Hay algún caso para hacer eso. Pero retrocedamos un paso aquí. El propósito del operador await es construir un flujo de trabajo asincrónico utilizando las convenciones de codificación de un flujo de trabajo síncrono . Entonces, ¿en qué pensar es cuál es ese flujo de trabajo ? Un flujo de trabajo impone un orden sobre un conjunto de tareas relacionadas.

La forma más fácil de ver el orden requerido en un flujo de trabajo es examinar la dependencia de los datos . No puede preparar el sándwich antes de que la tostada salga de la tostadora, por lo que tendrá que obtener el pan en algún lugar . Ya que espera extraer el valor de la tarea completada, tiene que haber una espera en algún lugar entre la creación de la tarea de la tostadora y la creación del emparedado.

También puede representar dependencias en los efectos secundarios. Por ejemplo, el usuario presiona el botón, por lo que desea reproducir el sonido de la sirena, luego espere tres segundos, luego abra la puerta, espere tres segundos y luego cierre la puerta:

DisableButton(); PlaySiren(); await Task.Delay(3000); OpenDoor(); await Task.Delay(3000); CloseDoor(); EnableButton();

No tendría ningún sentido decirlo

DisableButton(); PlaySiren(); var delay1 = Task.Delay(3000); OpenDoor(); var delay2 = Task.Delay(3000); CloseDoor(); EnableButton(); await delay1; await delay2;

Porque este no es el flujo de trabajo deseado.

Por lo tanto, la respuesta real a su pregunta es: postergar la espera hasta el punto en que realmente se necesita el valor es una práctica bastante buena, ya que aumenta las oportunidades de programar el trabajo de manera eficiente. Pero puedes ir demasiado lejos; asegúrese de que el flujo de trabajo que se implementa es el flujo de trabajo que desea.


En general, esto se debe a que una vez que las funciones asincrónicas se reproducen mejor con otras funciones asincrónicas, de lo contrario, comienzas a perder los beneficios de la asincronicidad. Como resultado, las funciones que llaman a las funciones asíncronas terminan siendo asíncronas y se propagan a lo largo de toda la aplicación, por ejemplo. si realizó sus interacciones con una tienda de datos asincrónica, entonces las cosas que utilizan esa funcionalidad también tienden a realizarse como asincrónicas.

A medida que convierta el código síncrono en código asíncrono, encontrará que funciona mejor si las llamadas de código asíncronas son llamadas por otro código asíncrono: todo el camino hacia abajo (o "arriba", si lo prefiere). Otros también han notado el comportamiento de propagación de la programación asincrónica y lo han llamado "contagioso" o lo han comparado con un virus zombie. Ya sea que se trate de tortugas o zombies, definitivamente es cierto que el código asincrónico tiende a conducir el código circundante para que también sea asincrónico. Este comportamiento es inherente a todos los tipos de programación asincrónica, no solo a las nuevas palabras clave async / await.

Fuente: Async / Await: mejores prácticas en programación asincrónica


Es un Actor Model World, realmente ...

Mi opinión es que async / await son simplemente una forma de disfrazar los sistemas de software para evitar tener que admitir que, en realidad, muchos sistemas (especialmente aquellos con muchas comunicaciones de red) se ven mejor como modelo Actor (o mejor) aún, Comunicando el Proceso Secuencial) sistemas.

Con ambos, el punto es que esperas que una de varias cosas llegue a ser completa, actúas cuando sea necesario y luego vuelves a esperar. Específicamente, está esperando que llegue un mensaje desde otro lugar, leyéndolo y actuando sobre el contenido. En * nix, la espera generalmente se realiza con una llamada a epoll () o select ().

El uso de await / async es simplemente una manera de pretender que su sistema todavía tiene llamadas de método sincrónico (y, por lo tanto, familiares), al mismo tiempo que hace que sea difícil hacer frente a cosas que no siempre se completan en el mismo orden.

Sin embargo, una vez que superas la idea de que ya no estás llamando a los métodos, sino simplemente pasando mensajes de un lado a otro, todo se vuelve muy natural. Es mucho más un "por favor haz esto", "seguro, aquí está la respuesta", con muchas de esas interacciones entrelazadas. Completarlo con una gran llamada WaitForLotsOfThings () en la parte superior de un bucle es simplemente un reconocimiento explícito de que su programa esperará hasta que tenga algo que hacer en respuesta a muchos otros programas que se comunican con él.

Cómo Windows lo dificulta

Desafortunadamente, Windows hace que sea muy difícil implementar un sistema proactor ("si lees ese mensaje ahora, lo obtendrás"). Windows es reactor ("ese mensaje que me pediste que leyera? Ahora lo he leído"). Es una distinción importante.

Con la primera, un mensaje (o incluso un tiempo de espera) que significa "dejar de escuchar a ese otro actor" es fácil de tratar: simplemente excluya a ese otro actor de la lista que escuchará la próxima vez que espere.

Con un reactor, es mucho más difícil. ¿Cómo se respeta el mensaje de "dejar de escuchar a ese otro actor" cuando la lectura ya se inició con algún tipo de llamada asincrónica, y no se completará hasta que se lea algo, un resultado dudoso debido a las instrucciones recibidas recientemente?

Estoy picando hasta cierto punto. Proactor es muy útil en sistemas con conectividad dinámica, los actores caen en el sistema, caen de nuevo. Reactor está bien si tienes una población fija de actores con enlaces de comunicación que nunca desaparecerán. Sin embargo, dado que un sistema de reactor se implementa fácilmente en una plataforma de proactor, pero un sistema de proactor no puede implementarse fácilmente en una plataforma de reactor (el tiempo no va hacia atrás), encuentro el enfoque de Window particularmente irritante.

De una forma u otra, async / await definitivamente están aún en la tierra del reactor.

Knock on Impact

Esto ha infectado muchas otras bibliotecas.

El Boost asio de C ++ también es reactor, incluso en * nix, en gran medida parece porque querían tener una implementación de Windows.

ZeroMQ, que es un marco de trabajo proactor, se limita en cierta medida a que Windows se base en una llamada a select () (que en Windows solo funciona en sockets).

Para la familia cygwin de los tiempos de ejecución POSIX en Windows, tuvieron que implementar select (), epoll (), etc. al tener un hilo por encuesta de descriptor de archivo (sí, ¡ polling !!!!) el socket / puerto serie / tubería subyacente para datos entrantes para recrear las rutinas de POSIX. Yeurk! Los comentarios en las listas de correo de cygwin dev que datan de la época en que estaban implementando esa parte hacen que la lectura sea entretenida.

El actor no es necesariamente lento

Vale la pena señalar que la frase "pasar mensajes" no significa necesariamente pasar copias: hay muchas formulaciones del Actor Model en las que simplemente se transfiere la propiedad de las referencias a los mensajes (por ejemplo, Dataflow, parte de la biblioteca Task Parallel en DO#). Esto lo hace rápido. Todavía no me he familiarizado con la biblioteca de Dataflow, pero en realidad no hace que Windows sea un factor de repente. No le da un sistema de modelo de actor actor que trabaje en todo tipo de portadores de datos como tomas de corriente, tuberías, colas, etc.

Tiempo de ejecución de Linux de Windows 10

Entonces, después de haber criticado Windows y su arquitectura de reactor inferior, un punto intrigante es que Windows 10 ahora ejecuta binarios de Linux. Me gustaría saber si Microsoft implementó la llamada al sistema que subyace a select (), epoll () dado que tiene que funcionar en sockets, puertos serie, tuberías y todo lo demás en la tierra de POSIX que es una descriptor de archivo, cuando todo lo demás en Windows no puede? Le daría mis dientes posteriores para saber la respuesta a esa pregunta.