¿Es un tipo híbrido de cosas? (por ejemplo, ¿mi programa .NET usa una pila hasta que llega a una llamada asíncrona y luego cambia a otra estructura hasta que se completa, en cuyo punto la pila se desenrolla a un estado en el que puede estar seguro de los siguientes elementos, etc.? )
Básicamente sí.
Supongamos que tenemos
async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }
Aquí hay una explicación extremadamente simplificada de cómo se reifican las continuaciones. El código real es considerablemente más complejo, pero esto transmite la idea.
Haces clic en el botón. Un mensaje está en cola. El bucle de mensajes procesa el mensaje y llama al controlador de clics, colocando la dirección de retorno de la cola de mensajes en la pila. Es decir, lo que sucede después de que se realiza el controlador es que el bucle de mensajes debe seguir ejecutándose. Entonces la continuación del controlador es el ciclo.
El controlador de clics llama a Foo (), colocando la dirección de retorno de sí mismo en la pila. Es decir, la continuación de Foo es el resto del controlador de clics.
Foo llama a Task.Delay, colocando la dirección de retorno de sí mismo en la pila.
Task.Delay hace lo que sea necesario para devolver inmediatamente una tarea. La pila está abierta y estamos de vuelta en Foo.
Foo comprueba la tarea devuelta para ver si se ha completado. No lo es. La continuación de la espera es llamar a Blah (), por lo que Foo crea un delegado que llama a Blah (), y firma ese delegado como la continuación de la tarea. (Acabo de hacer una pequeña declaración errónea; ¿lo entendiste? Si no, lo revelaremos en un momento).
Luego, Foo crea su propio objeto Tarea, lo marca como incompleto y lo devuelve a la pila al controlador de clics.
El controlador de clics examina la tarea de Foo y descubre que está incompleta. La continuación de la espera en el controlador es llamar a Bar (), por lo que el controlador de clic crea un delegado que llama a Bar () y lo establece como la continuación de la tarea devuelta por Foo (). Luego devuelve la pila al bucle de mensajes.
El bucle de mensajes sigue procesando mensajes. Finalmente, la magia del temporizador creada por la tarea de retraso hace lo suyo y publica un mensaje en la cola que dice que la continuación de la tarea de retraso ahora se puede ejecutar. Entonces, el bucle de mensajes llama a la continuación de la tarea, poniéndose en la pila como de costumbre. Ese delegado llama a Blah (). Blah () hace lo que hace y regresa a la pila.
Ahora que pasa? Aquí está la parte difícil. La continuación de la tarea de retraso no solo llama a Blah (). También tiene que activar una llamada a Bar () , ¡pero esa tarea no sabe sobre Bar!
Foo en realidad creó un delegado que (1) llama a Blah () y (2) llama a la continuación de la tarea que Foo creó y devolvió al controlador de eventos. Así es como llamamos a un delegado que llama a Bar ().
Y ahora hemos hecho todo lo que teníamos que hacer, en el orden correcto. Pero nunca dejamos de procesar mensajes en el bucle de mensajes durante mucho tiempo, por lo que la aplicación siguió respondiendo.
Que estos escenarios son demasiado avanzados para una pila tiene mucho sentido, pero ¿qué reemplaza a la pila?
Un gráfico de objetos de tareas que contienen referencias entre sí a través de las clases de cierre de los delegados. Esas clases de cierre son máquinas de estado que realizan un seguimiento de la posición de la espera ejecutada más recientemente y los valores de los locales. Además, en el ejemplo dado, una cola de acciones de estado global implementada por el sistema operativo y el bucle de mensajes que ejecuta esas acciones.
Ejercicio: ¿cómo supones que todo esto funciona en un mundo sin bucles de mensajes? Por ejemplo, aplicaciones de consola. esperar en una aplicación de consola es bastante diferente; ¿Puedes deducir cómo funciona a partir de lo que sabes hasta ahora?
Cuando me enteré de esto hace años, la pila estaba allí porque era ultrarrápida y liviana, una pieza de memoria asignada en la aplicación lejos del montón porque admitía una administración altamente eficiente para la tarea en cuestión (¿juego de palabras?). Que ha cambiado
Las pilas son una estructura de datos útil cuando las vidas de las activaciones de métodos forman una pila, pero en mi ejemplo las activaciones del controlador de clic, Foo, Bar y Blah no forman una pila. Y, por lo tanto, la estructura de datos que representa ese flujo de trabajo no puede ser una pila; más bien es un gráfico de tareas y delegados asignados al montón que representa un flujo de trabajo. Las esperas son los puntos en el flujo de trabajo donde no se puede avanzar más en el flujo de trabajo hasta que se haya completado el trabajo iniciado anteriormente; mientras esperamos, podemos ejecutar otro trabajo que no dependa de que esas tareas iniciadas en particular se hayan completado.
La pila es solo una matriz de cuadros, donde los cuadros contienen (1) punteros al centro de las funciones (donde ocurrió la llamada) y (2) valores de variables locales y temps. Las continuaciones de tareas son lo mismo: el delegado es un puntero a la función y tiene un estado que hace referencia a un punto específico en el medio de la función (donde ocurrió la espera), y el cierre tiene campos para cada variable local o temporal . Los marcos simplemente ya no forman una buena matriz ordenada, pero toda la información es la misma.