El patrón async-await de .net 4.5 está cambiando de paradigma. Es demasiado bueno para ser verdad.
He estado transfiriendo un código pesado de IO a async-await porque el bloqueo es cosa del pasado.
Muchas personas están comparando async-await con una infestación de zombis y me pareció bastante preciso. Al código asíncrono le gustan otros códigos asíncronos (necesita una función asíncrona para esperar en una función asíncrona). Entonces, cada vez más funciones se vuelven asincrónicas y esto sigue creciendo en su base de código.
Cambiar funciones a asincrónicas es un trabajo algo repetitivo y poco imaginativo. Introduzca una async
palabra clave en la declaración, ajuste el valor de retorno Task<>
y ya está. Es bastante inquietante lo fácil que es todo el proceso, y muy pronto un script 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 asíncrono lentamente, ¿por qué no hacerlo todo asíncrono por defecto?
La razón obvia que asumo es el rendimiento. Async-await tiene una sobrecarga y un código que no necesita ser asíncrono, preferiblemente no debería. Pero si el rendimiento es el único problema, seguramente algunas optimizaciones inteligentes pueden eliminar la sobrecarga automáticamente cuando no es necesaria. He leído sobre la optimización de la "ruta rápida" y me parece que solo ella debería encargarse de la mayor parte.
Quizás esto sea comparable al cambio de paradigma provocado por los recolectores de basura. En los primeros días de GC, liberar su propia memoria era definitivamente más eficiente. Pero las masas aún eligieron la recopilación automática en favor de un código más seguro y simple que podría ser menos eficiente (e incluso eso posiblemente ya no sea cierto). ¿Quizás este debería ser el caso aquí? ¿Por qué no todas las funciones deberían ser asincrónicas?
fuente
async
(para cumplir con algún contrato), esta probablemente sea una mala idea. Obtiene las desventajas de async (mayor costo de las llamadas a métodos; la necesidad de usarloawait
en el código de llamada), pero ninguna de las ventajas.Respuestas:
En primer lugar, gracias por sus amables palabras. De hecho, es una característica increíble y me alegro de haber sido una pequeña parte de ella.
Bueno, estás exagerando; todo su código no se vuelve asincrónico. Cuando sumas dos números enteros "simples", no estás esperando el resultado. Cuando sumas dos números enteros futuros juntos para obtener un tercer entero futuro , porque eso es lo que
Task<int>
es, es un número entero al que tendrás acceso en el futuro, por supuesto, es probable que estés esperando el resultado.La razón principal para no hacer que todo sea asincrónico es porque el propósito 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 recibir el impacto del rendimiento que mitiga esa latencia. Más bien, algunas de sus operaciones clave son de alta latencia, y esas operaciones están causando la infestación zombie de async en todo el código.
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 seguida de un pase de optimización.
El primer punto nuevamente es: async en C # / VB / F # es esencialmente una forma limitada de paso de continuación . Se ha realizado una enorme cantidad de investigación en la comunidad del lenguaje funcional para encontrar formas de identificar cómo optimizar el código que hace un uso intensivo del estilo de paso continuo. El equipo del compilador probablemente tendría que resolver problemas muy similares en un mundo donde "async" era el valor predeterminado y los métodos no async tenían que ser identificados y desincronizados. El equipo de C # no está realmente interesado en abordar problemas de investigación abiertos, por lo que hay grandes puntos en contra.
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 que el valor de una expresión no depende cuando se evalúa . Expresiones como
2 + 2
son referencialmente transparentes; puede realizar 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 comox+y
no se puede mover en el tiempo porque xey pueden cambiar con el tiempo .Async hace que sea mucho más difícil razonar sobre cuándo sucederá un efecto secundario. Antes de async, si dijiste:
y
M()
fuevoid M() { Q(); R(); }
,N()
fuevoid N() { S(); T(); }
yR
yS
produce efectos secundarios, entonces sabes que el efecto secundario de R ocurre antes que el efecto secundario de S. Pero si esasync void M() { await Q(); R(); }
así, de repente eso se va por la ventana. No tiene garantía de siR()
sucederá antes o despuésS()
(a menos que, por supuesto,M()
se espere; pero, por supuesto,Task
no es necesario esperarlo hasta despuésN()
).Ahora imagine que esta propiedad de no saber más en qué orden ocurren los efectos secundarios se aplica a cada fragmento de código en su programa, excepto a aquellos que el optimizador logra desincronizar. Básicamente, ya no tienes ni idea de qué expresiones se evaluarán en qué orden, lo que significa que todas las expresiones deben ser referencialmente transparentes, lo cual es difícil en un lenguaje como C #.
Un tercer punto en contra es que luego debe preguntarse "¿por qué la asíncrona es tan especial?" Si va a argumentar que cada operación debería ser realmente una,
Task<T>
entonces debe poder responder la pregunta "¿por qué noLazy<T>
?" o "¿por qué noNullable<T>
?" o "¿por qué noIEnumerable<T>
?" Porque podríamos hacer eso con la misma facilidad. ¿Por qué no debería ser el caso de que todas las operaciones pasen a ser anulables ? O cada operación se calcula de forma perezosa y el resultado se almacena en caché para más tarde , o el resultado de cada operación es una secuencia de valores en lugar de un solo valor . Luego debe intentar optimizar aquellas situaciones en las que sabe "oh, esto nunca debe ser nulo, para que pueda generar un mejor código", y así sucesivamente.El punto es: no me
Task<T>
queda claro que sea realmente tan especial como para justificar tanto trabajo.Si este tipo de cosas le interesan, le recomiendo que investigue lenguajes funcionales como Haskell, que tienen una transparencia referencial mucho más fuerte y permiten todo tipo de evaluación desordenada y almacenamiento en caché automático. Haskell también tiene un soporte mucho más fuerte en su sistema de tipos para los tipos de "levantamientos monádicos" a los que he aludido.
fuente
async/await
porque algo oculto bajo capas de abstracción puede ser asincrónico.El rendimiento es una de las razones, como mencionaste. 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 completa, 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 se volvería problemática.
El otro es una cuestión de complejidad e intención. Las operaciones asincrónicas agregan complejidad; en muchos casos, las características del lenguaje lo ocultan, pero hay muchos casos en los que la creación de 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 asincrónicos pueden terminar fácilmente causando problemas de subprocesos inesperados.Además, hay muchas rutinas que, por su naturaleza, no son asincrónicas. Esas tienen más sentido como operaciones sincrónicas. Obligar
Math.Sqrt
a serloTask<double> Math.SqrtAsync
sería ridículo, por ejemplo, ya que no hay ninguna razón para que eso sea asincrónico. En lugar deasync
impulsar su aplicación, terminaríaawait
propagándose por todas partes .Esto también rompería por completo el paradigma actual, además de causar problemas con las propiedades (que en realidad son solo pares de métodos ... ¿también se volverían asíncronos?), Y tendría otras repercusiones en todo el diseño del marco y el lenguaje.
Si está haciendo mucho trabajo vinculado a IO, tenderá a encontrarlo usando
async
generalizado es una gran adición, muchas de sus rutinas lo seránasync
. Sin embargo, cuando comienzas a hacer un trabajo vinculado a la CPU, en general, hacer las cosasasync
no es bueno: está ocultando el hecho de que estás usando ciclos de CPU bajo una API que parece ser asíncrona, pero en realidad no es necesariamente realmente asincrónica.fuente
await FooAsync()
más simple queFoo()
? ¿Y en lugar de un pequeño efecto dominó algunas veces, tienes un gran efecto dominó todo el tiempo y lo llamas una mejora?Dejando a un lado el rendimiento, la asíncrona puede tener un costo de productividad. En el cliente (WinForms, WPF, Windows Phone) es una bendición para la productividad. Pero en el servidor, o en otros escenarios que no sean de UI, pagas productividad. Ciertamente no querrás ir asincrónico por defecto allí. Úselo cuando necesite las ventajas de escalabilidad.
Úselo cuando esté en el punto óptimo. En otros casos, no lo haga.
fuente
Creo que hay una buena razón para hacer que todos los métodos sean asincrónicos si no son necesarios: extensibilidad. Los métodos de creación selectiva asíncronos solo funcionan si su código nunca evoluciona y 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 asíncrono).
Pero ¿y si las cosas cambian? Sí, A () está haciendo cálculos, pero en algún momento en el futuro tuvo que agregar registro allí, o informes, 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álculos de CPU sino también algunas E / S? Deberá convertir el método a asíncrono, pero esto rompería la API y todas las personas que llaman en la pila también deberían actualizarse (e incluso pueden ser diferentes aplicaciones de diferentes proveedores). O deberá agregar la versión asíncrona junto con la versión sincronizada, pero esto no hace mucha diferencia: el uso de la versión sincronizada se bloquearía y, por lo tanto, no es aceptable.
Sería genial si fuera posible hacer que el método de sincronización existente sea asíncrono sin cambiar la API. Pero en realidad no tenemos esa opción, creo, y usar la versión asíncrona incluso si no es necesaria actualmente es la única forma de garantizar que nunca tendrá problemas de compatibilidad en el futuro.
fuente