Responderé a sus preguntas específicas a continuación, pero probablemente haría bien en simplemente leer mis extensos artículos sobre cómo diseñamos rendimiento y espera.
https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/
https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/
https://blogs.msdn.microsoft.com/ericlippert/tag/async/
Algunos de estos artículos están desactualizados ahora; el código generado es diferente en muchos sentidos. Pero estos sin duda le darán una idea de cómo funciona.
Además, si no comprende cómo se generan las lambdas como clases de cierre, comprenda eso primero . No obtendrá ni cara ni cola de async si no tiene lambdas abajo.
Cuando se alcanza una espera, ¿cómo sabe el tiempo de ejecución qué fragmento de código debe ejecutarse a continuación?
await
se genera como:
if (the task is not completed)
assign a delegate which executes the remainder of the method as the continuation of the task
return to the caller
else
execute the remainder of the method now
Eso es básicamente todo. Esperar es solo un regreso elegante.
¿Cómo sabe cuándo puede reanudarse donde lo dejó y cómo recuerda dónde?
Bueno, ¿cómo haces eso sin esperar? Cuando el método foo llama a la barra de métodos, de alguna manera recordamos cómo volver al medio de foo, con todos los locales de la activación de foo intactos, sin importar lo que haga la barra.
Ya sabes cómo se hace en ensamblador. Se inserta un registro de activación para foo en la pila; contiene los valores de los lugareños. En el momento de la llamada, la dirección de retorno en foo se inserta en la pila. Cuando se termina la barra, el puntero de pila y el puntero de instrucción se restablecen a donde deben estar y foo continúa desde donde lo dejó.
La continuación de una espera es exactamente la misma, excepto que el registro se coloca en el montón por la razón obvia de que la secuencia de activaciones no forma una pila .
El delegado que aguarda da como continuación a la tarea contiene (1) un número que es la entrada a una tabla de búsqueda que da el puntero de instrucción que necesita ejecutar a continuación, y (2) todos los valores de locales y temporales.
Hay algo de equipo adicional allí; por ejemplo, en .NET es ilegal bifurcarse en el medio de un bloque try, por lo que no puede simplemente pegar la dirección del código dentro de un bloque try en la tabla. Pero estos son detalles contables. Conceptualmente, el registro de activación simplemente se traslada al montón.
¿Qué sucede con la pila de llamadas actual? ¿Se guarda de alguna manera?
La información relevante en el registro de activación actual nunca se coloca en la pila en primer lugar; se asigna del montón desde el principio. (Bueno, los parámetros formales se pasan en la pila o en los registros normalmente y luego se copian en una ubicación del montón cuando comienza el método).
Los registros de activación de las personas que llaman no se almacenan; la espera probablemente volverá a ellos, recuerde, por lo que se tratará con normalidad.
Tenga en cuenta que esta es una diferencia importante entre el estilo de paso de continuación simplificado de await y las estructuras de llamada con continuación actual que se ven en lenguajes como Scheme. En esos idiomas, call-cc captura toda la continuación, incluida la continuación hacia las personas que llaman .
¿Qué pasa si el método de llamada realiza otras llamadas antes de esperar? ¿Por qué no se sobrescribe la pila?
Esas llamadas a métodos regresan, por lo que sus registros de activación ya no están en la pila en el momento de la espera.
¿Y cómo diablos funcionaría el tiempo de ejecución a través de todo esto en el caso de una excepción y una pila desenrollada?
En el caso de una excepción no detectada, la excepción se captura, se almacena dentro de la tarea y se vuelve a lanzar cuando se obtiene el resultado de la tarea.
¿Recuerdas toda esa contabilidad que mencioné antes? Obtener la semántica de excepciones correctamente fue un gran dolor, déjame decirte.
Cuando se alcanza el rendimiento, ¿cómo realiza el tiempo de ejecución un seguimiento del punto donde se deben recoger las cosas? ¿Cómo se conserva el estado del iterador?
Mismo camino. El estado de los locales se traslada al montón y MoveNext
se almacena junto con los locales un número que representa la instrucción en la que debe reanudarse la próxima vez que se llame.
Y nuevamente, hay un montón de equipo en un bloque de iteradores para asegurarse de que las excepciones se manejen correctamente.