2012-03-22 18 views
11

Después de leer Eric Lippert’s answer, tengo la impresión de que await y call/cc son más o menos las dos caras de la misma moneda, con la mayoría de las diferencias sintácticas. Sin embargo, al tratar de implementar realmente call/cc en C# 5, me encontré con un problema: o entiendo mal call/cc (que es bastante posible), o aguardar es sólo con reminiscencias de call/cc.C# aguarden vs continuaciones: ¿no es lo mismo?

Considérese la pseudo-código como este:

function main: 
    foo(); 
    print "Done" 

function foo: 
    var result = call/cc(bar); 
    print "Result: " + result; 

function bar(continuation): 
    print "Before" 
    continuation("stuff"); 
    print "After" 

Si mi comprensión de llamada/cc es correcta, entonces esto debe imprimir:

Before 
Result: stuff 
Done 

crucial, cuando la continuidad se llama, el programa el estado se restablece junto con el historial de llamadas, por lo que foo vuelve a main y nunca vuelve a bar.

Sin embargo, si se implementa utilizando await en C#, llamar a la continuación no restaura este historial de llamadas. foo vuelve a bar, y no hay manera (que yo pueda ver) de que await se pueda usar para hacer que el historial de llamadas sea parte de la continuación.

Por favor, explique: ¿entendí mal el funcionamiento de call/cc, o await simplemente no es lo mismo que call/cc?


Ahora que sé la respuesta, debo decir que hay una buena razón para pensar que son bastante similares. Considere lo que el programa anterior se ve como en la pseudo-C# -5:

function main: 
    foo(); 
    print "Done" 

async function foo: 
    var result = await(bar); 
    print "Result: " + result; 

async function bar(): 
    print "Before" 
    return "stuff"; 
    print "After" 

Así, mientras que el estilo C# 5 nunca nos da un objeto de continuación para pasar un valor de, en general, la similitud es bastante sorprendente. Excepto que esta vez es totalmente obvio que "Después" nunca se llama, a diferencia del ejemplo de la llamada real/cc, que es otra razón para amar a C# y alabar su diseño.

+0

Timwi es correcto; esperar es más como un llamado de "efecto local"/cc; esta es una sutileza que no pensé llamar en mi respuesta original. Lo he actualizado –

Respuesta

18

await es realmente no es lo mismo que call/cc.

El tipo de call/cc totalmente fundamental en el que está pensando tendría que guardar y restaurar toda la pila de llamadas. Pero await es solo una transformación en tiempo de compilación. Hace algo similar, pero no usa la pila de llamadas real.

Imagine que tiene una función asíncrona que contiene una expresión esperar:

async Task<int> GetInt() 
{ 
    var intermediate = await DoSomething(); 
    return calculation(intermediate); 
} 

Ahora imaginemos que la función se llama a través de await contiene un await expresión:

async Task<int> DoSomething() 
{ 
    var important = await DoSomethingImportant(); 
    return un(important); 
} 

Ahora piense en lo sucede cuando termina DoSomethingImportant() y su resultado está disponible. El control vuelve al DoSomething(). Entonces DoSomething() termina y ¿qué ocurre entonces?El control vuelve al GetInt(). El comportamiento es exactamente como sería si GetInt() estaban en la pila de llamadas. Pero no es realmente; tiene que usar await en cada llamada a que desee simular de esta manera. Por lo tanto, la pila de llamadas se levanta en un meta-call-stack que se implementa en el awaiter.

Lo mismo, por cierto, es cierto de yield return:

IEnumerable<int> GetInts() 
{ 
    foreach (var str in GetStrings()) 
     yield return computation(str); 
} 

IEnumerable<string> GetStrings() 
{ 
    foreach (var stuff in GetStuffs()) 
     yield return computation(stuff); 
} 

Ahora si llamo GetInts(), lo que se obtiene es un objeto que encapsula el estado actual de ejecución GetInts() (de modo que llamar MoveNext() en ella se reanuda operación donde lo dejó). Este objeto en sí contiene el iterador que se itera a través de GetStrings() y llama al MoveNext() en que. Por lo tanto, la pila de llamadas real se reemplaza por una jerarquía de objetos que recrean la pila de llamadas correcta cada vez a través de una serie de llamadas al MoveNext() en el siguiente objeto interno.

+0

Por cierto, cuando depura una sincronización, ve MoveNext como nombre de método en el punto de interrupción. Eso sugiere que usaron el generador de máquina de estado del iterador de IEnumerable. –

Cuestiones relacionadas