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.
Si todo mi código se está volviendo asíncrono lentamente, ¿por qué no hacerlo todo asíncrono de forma predeterminada?
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.
Si el rendimiento es el único problema, seguramente algunas optimizaciones inteligentes pueden eliminar la sobrecarga automáticamente cuando no es necesaria.
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 como x+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:
M();
N();
y M()
fue void M() { Q(); R(); }
, N()
fue void N() { S(); T(); }
y R
y S
produce efectos secundarios, entonces sabes que el efecto secundario de R ocurre antes que el efecto secundario de S. Pero si es async void M() { await Q(); R(); }
así, de repente eso se va por la ventana. No tiene garantía de si R()
sucederá antes o después S()
(a menos que, por supuesto, M()
se espere; pero, por supuesto, Task
no es necesario esperarlo hasta después N()
).
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é no Lazy<T>
?" o "¿por qué no Nullable<T>
?" o "¿por qué no IEnumerable<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.