C#await vs continuations:不太一样?

在阅读了Eric Lippert的回答后,我得到的印象是awaitcall/cc几乎是同一枚硬币的两面,最多只有语法差异。 但是,在尝试在C#5中实际实现call/cc时,我遇到了一个问题:要么我误解了call / cc(这是相当可能的),要么等待只是让人联想到call / cc。

考虑像这样的伪代码:

 function main: foo(); print "Done" function foo: var result = call/cc(bar); print "Result: " + result; function bar(continuation): print "Before" continuation("stuff"); print "After" 

如果我对call / cc的理解是正确的,那么应该打印:

 Before Result: stuff Done 

至关重要的是,当调用continuation时,程序状态将与调用历史一起恢复,以便foo返回main并且永远不会返回bar

但是,如果在C#中使用await实现,则调用continuation 不会还原此调用历史记录。 foo返回bar ,并且没有办法(我可以看到) await可用于使正确的调用历史记录成为continuation的一部分。

请解释一下:我是否完全误解了call/cc的操作,或awaitcall/cc不完全相同?


现在我知道了答案,我不得不说有充分的理由认为它们非常相似。 考虑上面的程序在伪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" 

因此,虽然C#5样式永远不会给我们一个传递值的延续对象,但整体上相似性非常惊人。 除了这次,“After”永远不会被调用,这与真正的call / cc示例不同,这是另一个喜欢C#并赞美其设计的理由!

await确实与call/cc不完全相同。

您正在考虑的那种完全基本的call/cc确实必须保存并恢复整个调用堆栈。 但await只是一个编译时的转换。 它做了类似的事情,但没有使用真正的调用堆栈。

想象一下,你有一个包含await表达式的异步函数:

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

现在假设你通过await 自己调用的函数包含一个await表达式:

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

现在想想当DoSomethingImportant()完成并且结果可用时会发生什么。 控制返回DoSomething() 。 然后DoSomething()完成,然后会发生什么? 控制返回到GetInt() 。 行为与GetInt()在调用堆栈上的行为完全相同。 但事实并非如此; 你必须在每次通过这种方式模拟的电话中使用await 。 因此,调用栈被提升到在awaiter中实现的元调用栈中。

顺便提一下, yield return也是如此:

 IEnumerable GetInts() { foreach (var str in GetStrings()) yield return computation(str); } IEnumerable GetStrings() { foreach (var stuff in GetStuffs()) yield return computation(stuff); } 

现在,如果我调用GetInts() ,我得到的是一个封装GetInts()的当前执行状态的GetInts() (以便调用它上面的MoveNext()恢复它停止的操作)。 该对象本身包含迭代通过GetStrings()并在上调用MoveNext()的迭代器。 因此, 实际调用堆栈由对象层次结构代替,每次通过对下一个内部对象的一系列MoveNext()调用重新创建正确的调用堆栈。