remarks generate example c# .net asynchronous async-await

generate - params comments c#



¿Por qué no todas las funciones deben ser asincrónicas por defecto? (4)

El patrón async-await de .net 4.5 es un cambio de paradigma. Es casi demasiado bueno para ser verdad.

He estado portando un código IO-heavy para asincruar-aguardar porque el bloqueo es una cosa del pasado.

Un buen número de personas están comparando async-await con una infestación de zombies y me pareció bastante preciso. Al código asíncrono le gusta otro código asíncrono (necesita una función asíncrona para esperar en una función asíncrona). Así que cada vez más funciones se vuelven asíncronas y esto sigue creciendo en su base de código.

Cambiar las funciones a asincrónico es un trabajo algo repetitivo y poco imaginativo. Lanza una palabra clave async en la declaración, ajusta el valor devuelto por la Task<> y ya casi has terminado. Es bastante inquietante lo fácil que es todo el proceso, y muy pronto una secuencia de comandos de reemplazo de texto automatizará la mayor parte de la "migración" para mí.

Y ahora la pregunta ... Si todo mi código se está volviendo lentamente asincrónico, ¿por qué no simplemente hacer que todo sea asincrónico por defecto?

La razón obvia por la que supongo es el rendimiento. Async-await tiene una sobrecarga y un código que no necesita ser asincrónico, preferiblemente no debería. Pero si el rendimiento es el único problema, seguramente algunas optimizaciones inteligentes pueden eliminar la sobrecarga automáticamente cuando no se necesita. He leído sobre la optimización de "ruta rápida" , y me parece que solo debería ocuparse de la mayor parte.

Tal vez esto sea comparable al cambio de paradigma provocado por los recolectores de basura. En los primeros días de GC, liberar tu propia memoria era definitivamente más eficiente. Pero las masas aún eligen la recolección automática en favor de un código más seguro y simple que podría ser menos eficiente (e incluso eso podría decirse que ya no es cierto). Tal vez este debería ser el caso aquí? ¿Por qué no deberían todas las funciones ser asincrónicas?


¿Por qué no deberían todas las funciones ser asincrónicas?

El rendimiento es una de las razones, como has mencionado. Tenga en cuenta que la opción de "ruta rápida" a la que se vinculó mejora el rendimiento en el caso de una Tarea completada, pero aún requiere muchas más instrucciones y gastos generales en comparación con una sola llamada a un método. Como tal, incluso con la "ruta rápida" en su lugar, está agregando mucha complejidad y sobrecarga con cada llamada al método asíncrono.

La compatibilidad con versiones anteriores, así como la compatibilidad con otros lenguajes (incluidos los escenarios de interoperabilidad), también serían problemáticos.

El otro es una cuestión de complejidad e intención. Las operaciones asincrónicas añaden complejidad; en muchos casos, las características del idioma lo ocultan, pero hay muchos casos en los que hacer métodos async definitivamente agrega complejidad a su uso. Esto es especialmente cierto si no tiene un contexto de sincronización, ya que los métodos de sincronización pueden terminar causando problemas inesperados.

Además, hay muchas rutinas que, por su naturaleza, no son asíncronas. Esos tienen más sentido como operaciones sincrónicas. Obligar a Math.Sqrt a ser Task<double> Math.SqrtAsync sería ridículo, por ejemplo, ya que no hay ninguna razón para que sea asincrónico. En lugar de tener una async a través de su aplicación, terminaría await proponerse en todas partes .

Esto también rompería por completo el paradigma actual, y causaría problemas con las propiedades (que en realidad son solo pares de métodos ... ¿también se sincronizarían?) Y tendría otras repercusiones a lo largo del diseño del marco y el lenguaje.

Si está trabajando mucho en IO, tenderá a descubrir que utilizar async es una gran adición, muchas de sus rutinas serán async . Sin embargo, cuando comienzas a trabajar en CPU, en general, hacer que las cosas sean async realidad no es bueno, está ocultando el hecho de que estás usando ciclos de CPU bajo una API que parece ser asincrónica, pero que realmente no es necesariamente asíncrona.


Creo que hay una buena razón para hacer todos los métodos asincrónicos si no son necesarios para la extensibilidad. Los métodos de creación selectiva asíncronos solo funcionan si su código nunca evoluciona y usted sabe que el método A () siempre está vinculado a la CPU (lo mantiene sincronizado) y el método B () siempre está vinculado a E / S (lo marca como sincronización).

Pero, ¿y si las cosas cambian? Sí, A () está haciendo cálculos, pero en algún momento en el futuro tuvo que agregar el registro allí, o informar, o devolución de llamada definida por el usuario con implementación que no puede predecir, o el algoritmo se ha extendido y ahora incluye no solo cómputos de CPU sino también algo de E / S? Necesitarás convertir el método a asíncrono, pero esto rompería la API y todos los llamantes de la pila también tendrían que actualizarse (e incluso pueden ser diferentes aplicaciones de diferentes proveedores). O tendrá que agregar la versión asíncrona junto con la versión de sincronización, pero esto no tiene mucha importancia, el uso de la versión de sincronización bloquearía y, por lo tanto, no es aceptable.

Sería fantástico si fuera posible hacer que el método de sincronización existente fuera asincrónico sin cambiar la API. Pero en realidad no tenemos esa opción, creo, y el uso de la versión asíncrona, incluso si no se necesita actualmente, es la única manera de garantizar que nunca se verán afectados por problemas de compatibilidad en el futuro.


Primero, gracias por tus amables palabras. De hecho, es una característica increíble y estoy contento de haber sido una pequeña parte de ella.

Si todo mi código se está volviendo lentamente asincrónico, ¿por qué no hacerlo todo de manera predeterminada?

Bueno, estás exagerando; todo tu código no se está volviendo asincrónico. Cuando agrega dos enteros "simples", no está esperando el resultado. Cuando agrega dos enteros futuros juntos para obtener un tercer número entero futuro , porque eso es lo que Task<int> , es un número entero al que tendrá acceso en el futuro, por supuesto, probablemente estará esperando el número entero futuro. resultado.

La razón principal para no hacer que todo sea asincrónico es porque el objetivo de async / await es facilitar la escritura de código en un mundo con muchas operaciones de alta latencia . La gran mayoría de sus operaciones no tienen una latencia alta, por lo que no tiene sentido tomar el golpe de rendimiento que mitigue esa latencia. Por el contrario, algunas de sus operaciones son de alta latencia, y esas operaciones están causando la infestación de zombies de asincronización en todo el código.

si el rendimiento es el único problema, seguramente algunas optimizaciones inteligentes pueden eliminar la sobrecarga automáticamente cuando no se necesita.

En teoría, la teoría y la práctica son similares. En la práctica, nunca lo son.

Déjame darte tres puntos en contra de este tipo de transformación seguido de un pase de optimización.

El primer punto nuevamente es: asincrónico en C # / VB / F # es esencialmente una forma limitada de aprobación de continuación . Una gran cantidad de investigación en la comunidad del lenguaje funcional se ha dedicado a descubrir maneras de identificar cómo optimizar el código que hace un uso intensivo del estilo de continuación de aprobación. El equipo del compilador probablemente tendría que resolver problemas muy similares en un mundo donde "asincrónico" era el predeterminado y los métodos no asíncronos tenían que ser identificados y desinstalados. El equipo de C # no está realmente interesado en asumir problemas abiertos de investigación, así que eso es un gran punto en contra de eso.

Un segundo punto en contra es que C # no tiene el nivel de "transparencia referencial" que hace que este tipo de optimizaciones sea más manejable. Por "transparencia referencial" me refiero a la propiedad de la que el valor de una expresión no depende cuando se evalúa . Expresiones como 2 + 2 son referencialmente transparentes; puede hacer la evaluación en tiempo de compilación si lo desea, o diferirla hasta el tiempo de ejecución y obtener la misma respuesta. Pero una expresión como x+y no se puede mover en el tiempo porque xey podrían estar cambiando con el tiempo .

Async hace que sea mucho más difícil razonar sobre cuándo ocurrirá un efecto secundario. Antes de la sincronización, si dijiste:

M(); N();

y M() fue void M() { Q(); R(); } void M() { Q(); R(); } void M() { Q(); R(); } , y N() fue void N() { S(); T(); } void N() { S(); T(); } void N() { S(); T(); } , y R y S producen efectos secundarios, entonces sabes que el efecto secundario de R ocurre antes que el efecto secundario de S. Pero si tiene el async void M() { await Q(); R(); } async void M() { await Q(); R(); } async void M() { await Q(); R(); } luego, de repente, eso sale por la ventana. No se garantiza si R() va a suceder antes o después de S() (a menos que se espere M() , pero por supuesto no es necesario esperar hasta después de N() .

Ahora imagina que esta propiedad de no saber en qué orden se producen los efectos secundarios se aplica a cada fragmento de código de tu programa, excepto a aquellos que el optimizador logra eliminar-asincronizar-ify. Básicamente ya no tienes idea de qué expresiones se evaluarán en qué orden, lo que significa que todas las expresiones deben ser referencialmente transparentes, lo que es difícil en un lenguaje como C #.

Un tercer punto en contra es que debes preguntar "¿por qué es tan especial?" Si vas a argumentar que cada operación debería ser en realidad una Task<T> entonces debes poder responder la pregunta "¿por qué no Lazy<T> ?" o "¿por qué no Nullable<T> ?" o "¿por qué no IEnumerable<T> ?" Porque podríamos hacer eso fácilmente. ¿Por qué no debería ser el caso de que cada operación se levante para anular ? O bien, cada operación se calcula de forma diferida y el resultado se guarda en caché para más adelante , o el resultado de cada operación es una secuencia de valores en lugar de un solo valor . Luego debe tratar de optimizar aquellas situaciones en las que sabe "oh, esto nunca debe ser nulo, por lo que puedo generar un mejor código", y así sucesivamente. (Y, de hecho, el compilador de C # lo hace para la aritmética levantada).

El punto es: no está claro para mí que Task<T> sea ​​realmente tan especial para justificar este trabajo.

Si este tipo de cosas le interesan, le recomiendo que investigue los lenguajes funcionales como Haskell, que tienen una transparencia referencial mucho más fuerte y permiten todo tipo de evaluación fuera de orden y hacen el almacenamiento en caché automático. Haskell también tiene un apoyo mucho más fuerte en su sistema de tipos para los tipos de "levantamientos monádicos" a los que he aludido.


Rendimiento a un lado - asincrónico puede tener un costo de productividad. En el cliente (WinForms, WPF, Windows Phone) es una gran ayuda para la productividad. Pero en el servidor, o en otros escenarios que no sean UI, usted paga productividad. Ciertamente no quiere ir de manera asincrónica por defecto allí. Úselo cuando necesite las ventajas de escalabilidad.

Úselo cuando esté en el punto óptimo. En otros casos, no.