¿Por qué necesitamos la palabra clave asíncrona?

19

Acabo de empezar a jugar con async / await en .Net 4.5. Una cosa que tengo curiosidad inicialmente es: ¿por qué es necesaria la palabra clave asíncrona? La explicación que leí fue que es un marcador, por lo que el compilador sabe que un método espera algo. Pero parece que el compilador debería poder resolver esto sin una palabra clave. Entonces, ¿qué más hace?

CondiciónRacer
fuente
2
Aquí hay un artículo de blog realmente bueno (serie) que detalla la palabra clave en C # y la funcionalidad relacionada en F #.
Paul
Creo que es principalmente un marcador para el desarrollador, y no para el compilador.
CodesInChaos
8
Este artículo lo explica: blogs.msdn.com/b/ericlippert/archive/2010/11/11/…
svick
@ Gracias gracias, esto es lo que estaba buscando. Tiene mucho sentido ahora.
ConditionRacer

Respuestas:

23

Aquí hay varias respuestas, y todas hablan de lo que hacen los métodos asíncronos, pero ninguna de ellas responde a la pregunta, por eso asyncse necesita como una palabra clave que figura en la declaración de la función.

No es "dirigir al compilador para transformar la función de una manera especial"; awaitsolo podría hacer eso. ¿Por qué? Debido a que C # ya tiene otro mecanismo en el que la presencia de una palabra clave especial en el cuerpo del método hace que el compilador para realizar extrema (y muy similar a async/await) transformaciones en el cuerpo del método: yield.

Excepto que yieldno es su propia palabra clave en C #, y entender por qué se explicará asynctambién. A diferencia de la mayoría de los lenguajes que admiten este mecanismo, en C # no se puede yield value;decir yield return value;. ¿Por qué? Debido a que se agregó al lenguaje después de que C # ya existía, y era bastante razonable suponer que alguien, en algún lugar, podría haber usado yieldel nombre de una variable. Pero debido a que no existía un escenario preexistente en el que <variable name> returnfuera sintácticamente correcto, yield returnse agregó al lenguaje para que sea posible introducir generadores mientras se mantiene la compatibilidad con el código existente al 100%.

Y es por eso que asyncse agregó como un modificador de función: para evitar romper el código existente que se usaba awaitcomo nombre de variable. Como asyncya no existían métodos, no se invalida ningún código antiguo, y en el nuevo código, el compilador puede usar la presencia de la asyncetiqueta para saber que awaitdebe tratarse como una palabra clave y no como un identificador.

Mason Wheeler
fuente
3
Esto es más o menos lo que dice este artículo también (publicado originalmente por @svick en los comentarios), pero nadie lo publicó como respuesta. ¡Solo tomó dos años y medio! Gracias :)
ConditionRacer
¿No significa eso que en alguna versión futura, el requisito de especificar la asyncpalabra clave se puede eliminar? Obviamente, sería una ruptura de BC, pero podría pensarse que afecta a muy pocos proyectos y es un requisito directo para actualizar (es decir, cambiar el nombre de una variable).
Preguntas de Quolonel
6

cambia el método de un método normal a un objeto con devolución de llamada que requiere un enfoque totalmente diferente para la generación de código

y cuando sucede algo drástico como eso, es costumbre indicarlo claramente (aprendimos esa lección de C ++)

monstruo de trinquete
fuente
¿Entonces esto es realmente algo para el lector / programador, y no tanto para el compilador?
ConditionRacer
@ Justin984 definitivamente ayuda a indicarle al compilador que algo especial tiene que suceder
fanático del trinquete
Supongo que tengo curiosidad por qué el compilador lo necesita. ¿No podría simplemente analizar el método y ver que esperar está en algún lugar del cuerpo del método?
ConditionRacer
que ayuda cuando se quiere programar múltiples asyncs al mismo tiempo por lo que no se ejecutan, uno tras otro, pero al mismo tiempo (si las roscas están disponibles por lo menos)
trinquete monstruo de
@ Justin984: no todos los métodos de devolución Task<T>tienen las características de un asyncmétodo; por ejemplo, puede ejecutar una lógica comercial / de fábrica y luego delegar en algún otro método de devolución Task<T>. asynclos métodos tienen un tipo de retorno Task<T>en la firma pero en realidad no devuelven a Task<T>, solo devuelven a T. Para resolver todo esto sin la asyncpalabra clave, el compilador de C # tendría que hacer todo tipo de inspección profunda del método, lo que probablemente lo ralentizaría un poco y generaría toda clase de ambigüedades.
Aaronaught
4

La idea general con palabras clave como "asíncrono" o "inseguro" es eliminar la ambigüedad en cuanto a cómo se debe tratar el código que modifican. En el caso de la palabra clave asíncrona, le dice al compilador que trate el método modificado como algo que no necesita regresar de inmediato. Esto permite que el hilo donde se usa este método continúe sin tener que esperar los resultados de ese método. Es efectivamente una optimización de código.

Ingeniero mundial
fuente
Estoy tentado a votar en contra porque aunque el primer párrafo es correcto, la comparación con las interrupciones es incorrecta (en mi opinión informada).
Paul
Los veo como algo análogo en términos de propósito, pero lo eliminaré por razones de claridad.
Ingeniero mundial
En mi opinión, las interrupciones se parecen más a los eventos que al código asíncrono: provocan un cambio de contexto y se ejecutan hasta su finalización antes de que se reanude el código normal (y el código normal también puede ignorar por completo su ejecución), mientras que con el código asincrónico el método podría no haber completado antes de que se reanude el hilo de llamada. Veo lo que estabas tratando de decir con diferentes mecanismos que requieren diferentes implementaciones, pero las interrupciones simplemente no coincidían conmigo para ilustrar este punto. (+1 para el resto, por cierto)
Paul
0

OK, aquí está mi opinión al respecto.

Hay algo llamado corutinas que se conoce desde hace décadas. ("Knuth and Hopper" -clase "durante décadas") Son generalizaciones de subrutinas , en las que no solo obtienen y liberan el control en el inicio de la función y la declaración de retorno, sino que también lo hacen en puntos específicos ( puntos de suspensión ). Una subrutina es una rutina sin puntos de suspensión.

Son FÁCILMENTE FÁCILES de implementar con macros C, como se muestra en el siguiente documento sobre "protothreads". ( http://dunkels.com/adam/dunkels06protothreads.pdf ) Léalo. Esperaré...

La conclusión de esto es que las macros crean una gran switchy una caseetiqueta en cada punto de suspensión. En cada punto de suspensión, la función almacena el valor de la caseetiqueta que sigue inmediatamente , para que sepa dónde reanudar la ejecución la próxima vez que se llame. Y devuelve el control a la persona que llama.

Esto se hace sin modificar el flujo aparente de control del código descrito en el "protothread".

Imagínese ahora que tiene un gran bucle que llama a todos estos "protothreads" a su vez, y obtiene simultáneamente "protothreads" en un solo hilo.

Este enfoque tiene dos inconvenientes:

  1. No puede mantener el estado en variables locales entre reinicios.
  2. No puede suspender el "protothread" de una profundidad de llamada arbitraria. (todos los puntos de suspensión deben estar en el nivel 0)

Hay soluciones para ambos:

  1. Todas las variables locales deben extraerse al contexto del prototipo (contexto que ya es necesario por el hecho de que el prototipo debe almacenar su próximo punto de reanudación)
  2. Si siente que realmente necesita llamar a otro protothread desde un protothread, "genere" un niño protothread y suspenda hasta la finalización del niño.

Y si tuviera el soporte del compilador para hacer el trabajo de reescritura que hacen las macros y la solución alternativa, bien, podría escribir su código de protothread tal como lo desea e insertar puntos de suspensión con una palabra clave.

Y esto es lo que asyncy awaitson todos acerca de: crear (sin pérdida de velocidad) corrutinas.

Las corutinas en C # se reifican como objetos de clase (genérica o no genérica) Task.

Encuentro estas palabras clave muy engañosas. Mi lectura mental es:

  • async como "suspensible"
  • await como "suspender hasta la finalización de"
  • Task como "futuro ..."

Ahora. ¿Realmente necesitamos marcar la función async? Además de decir que debería activar los mecanismos de reescritura de código para hacer que la función sea una rutina, resuelve algunas ambigüedades. Considera este código.

public Task<object> AmIACoroutine() {
    var tcs = new TaskCompletionSource<object>();
    return tcs.Task;
}

Suponiendo que asyncno sea obligatorio, ¿es esto una función de rutina o normal? ¿Debería el compilador reescribirlo como una rutina o no? Ambos podrían ser posibles con diferentes semánticas eventuales.

Laurent LA RIZZA
fuente