¿Cómo podría implementarse la nueva característica asíncrona en c#5.0 con call/cc?
asynchronous callcc (3)
Respuesta original:
Su pregunta, según tengo entendido, es "¿qué pasaría si en lugar de implementar" esperar "específicamente para la asincronía basada en tareas, más bien, se haya implementado la operación de flujo de control más general de la llamada con la continuación de la corriente?"
Bueno, primero que todo pensemos en lo que hace "esperar". "await" toma una expresión de tipo Task<T>
, obtiene un vigilante y llama al que espera con la continuación actual:
await FooAsync()
se convierte efectivamente
var task = FooAsync();
var awaiter = task.GetAwaiter();
awaiter.BeginAwait(somehow get the current continuation);
Ahora supongamos que tenemos un operador callcc
que toma como argumento un método y llama al método con la continuación actual. Eso se vería así:
var task = FooAsync();
var awaiter = task.GetAwaiter();
callcc awaiter.BeginAwait;
En otras palabras:
await FooAsync()
no es nada más que
callcc FooAsync().GetAwaiter().BeginAwait;
Eso responde tu pregunta?
Actualización # 1:
Como señala un comentarista, la respuesta a continuación asume el patrón de generación de código de la versión "Technology Preview" de la función async / await. En realidad, generamos un código ligeramente diferente en la versión beta de la característica, aunque lógicamente es lo mismo. El presente código es algo así como:
var task = FooAsync();
var awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
awaiter.OnCompleted(somehow get the current continuation);
// control now returns to the caller; when the task is complete control resumes...
}
// ... here:
result = awaiter.GetResult();
// And now the task builder for the current method is updated with the result.
Tenga en cuenta que esto es un poco más complicado y maneja el caso en el que está "esperando" un resultado que ya se ha calculado. No hay necesidad de pasar por todo el rigamarole de ceder el control a la persona que llama y continuar de nuevo donde lo dejó si el resultado que está esperando ya está en la memoria caché para usted.
Por lo tanto, la conexión entre "esperar" y "callcc" no es tan sencilla como lo fue en la versión preliminar, pero aún está claro que esencialmente estamos haciendo un callcc en el método "OnCompleted" del presentador. Simplemente no hacemos el callcc si no tenemos que hacerlo.
Actualización # 2:
Como esta respuesta
https://stackoverflow.com/a/9826822/88656
de Timwi señala, la semántica de call / cc y espera no son exactamente las mismas; una "verdadera" call / cc requiere que nosotros "capturemos" toda la continuación de un método, incluyendo toda su pila de llamadas , o, de manera equivalente, que todo el programa se reescriba en el estilo de paso de continuación.
La característica "esperar" es más como una "llamada cooperativa / cc"; la continuación solo captura "¿cuál es el método actual de devolución de tareas que se realizará a continuación en el punto de espera?" Si la persona que llama al método de devolución de tareas va a hacer algo interesante después de que se complete la tarea, entonces es libre de registrar su continuación como la continuación de la tarea.
He estado siguiendo el nuevo anuncio con respecto a la nueva característica de async
que estará en c # 5.0. Tengo una comprensión básica del estilo de paso de continuación y de la transformación que el nuevo compilador c # hace para codificar como este fragmento de la publicación de Eric Lippert :
async void ArchiveDocuments(List<Url> urls)
{
Task archive = null;
for(int i = 0; i < urls.Count; ++i)
{
var document = await FetchAsync(urls[i]);
if (archive != null)
await archive;
archive = ArchiveAsync(document);
}
}
Sé que algunos lenguajes implementan continuaciones de forma nativa, a través de call-with-current-continuation ( callcc
), pero realmente no entiendo cómo funciona eso o qué hace exactamente.
Así que aquí está la pregunta: si Anders et al. había decidido morder la bala y simplemente implementar callcc
en c # 5.0 en lugar del caso especial async
/ callcc
, ¿cómo sería el fragmento de código anterior?
No soy un experto en las continuaciones, pero intentaré explicar la diferencia entre async / await y call / cc. Por supuesto, esta explicación asume que entiendo call / cc y async / await, lo cual no estoy seguro de hacer. Sin embargo, aquí va ...
Con C # ''async'', le está diciendo al compilador que genere una versión especial de ese método específico que entienda cómo embotellar su estado en una estructura de datos de pila, para que pueda "eliminarse de la pila real" y reanudarla más tarde. Dentro de un contexto asíncrono, "esperar" es como "call / cc", ya que utiliza los objetos generados por el compilador para embotellar el estado y salir de la "pila real" hasta que la tarea se completa. Sin embargo, debido a que es la reescritura del compilador de un método asíncrono que permite embotellar el estado, await solo se puede usar en un contexto asíncrono.
En call / cc de primera clase, el tiempo de ejecución del lenguaje genera todo el código, de modo que la continuación actual se puede embotellar en una función de continuación de llamada (haciendo innecesaria la palabra clave asíncrona). call / cc todavía actúa como esperar, lo que provoca que el estado de continuación actual (piense en el estado de la pila) se reinicie y pase como una función a la función llamada. Una forma de hacer esto es usar marcos de pila para todas las invocaciones de funciones, en lugar de marcos de "pila". (a veces conocido como "sin pila", como en "python sin pila" o muchas implementaciones de esquemas) Otra forma es eliminar todos los datos de la "pila real" y colocarlos en una estructura de datos del montón antes de llamar al objetivo de call / cc .
Algunos problemas difíciles pueden surgir si hay llamadas a funciones externas (piense en DllImport) entremezcladas en la pila. Sospecho que esta es la razón por la que se fueron con la implementación async / await.
http://www.madore.org/~david/computers/callcc.html
Debido a que en C #, una función debe estar marcada como "asíncrona" para usar estos mecanismos, me pregunto si esta palabra clave asíncrona se convertirá en un virus que se propaga a muchas funciones en muchas bibliotecas. Si esto pasa. eventualmente se darán cuenta de que deben implementar call / cc de primera clase en el nivel de VM, en lugar de este modelo asíncrono basado en compilación y reescritura. Sólo el tiempo dirá. Sin embargo, es ciertamente una herramienta útil en el contexto del entorno C # actual.
Un poco inútil, diría yo. Los intérpretes del esquema a menudo implementan call / cc con una máquina de estado que captura el estado local en el montón. Que es exactamente lo que C # 5.0 (o más correctamente, el iterador de C # 2.0) también hace. Implementaron call / cc, la abstracción que crearon es bastante elegante.