C#中的await和continuations:并不完全相同?

14

看完Eric Lippert的回答后,我觉得awaitcall/cc基本上是同一个硬币的两面,最多只有语法上的区别。然而,在尝试在C# 5中实现call/cc时,我遇到了一个问题:要么我误解了call/cc(这很可能),要么await仅仅是回忆起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
关键是,当调用续体时,程序状态将被恢复 连同调用历史记录,以便 foo 返回到 main 并且不会回到 bar
然而,如果使用 C# 中的 await 来实现,调用续体不会恢复这个调用历史记录。 foo 返回到 bar,我无法看到任何方法可以使用 await 使正确的调用历史记录成为续体的一部分。
请解释:我是否完全误解了 call/cc 的操作,还是说 await 只是与 call/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#并赞扬其设计的另一个原因!


Timwi 是正确的;await 更像是一个“本地效果” call/cc;这是我在原始答案中没有想到要指出的微妙之处。我已经更新了它。 - Eric Lippert
1个回答

22

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

你想要的基本的call/cc类型确实需要保存并恢复整个调用栈。但是,await只是一种编译时转换。它执行类似的操作,但不使用真正的调用栈。

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

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

现在想象一下,你通过await调用的函数本身包含一个await表达式:

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

现在考虑一下当 DoSomethingImportant() 完成并且其结果可用时会发生什么。控制权返回到 DoSomething()。然后 DoSomething() 完成了,接着会发生什么呢?控制权返回到 GetInt()。其行为与如果 GetInt() 在调用栈上是完全相同的。但实际上并不是这样的;你必须在每个需要模拟这种方式的调用处使用 await。因此,调用栈被提升到一个元调用栈中,在等待者中实现。

顺便说一句,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);
}

如果我调用GetInts(),返回的将是一个封装了GetInts()当前执行状态的对象(这样在该对象上调用MoveNext()会从上次离开的地方恢复操作)。这个对象本身包含一个正在遍历GetStrings()并在其中调用MoveNext()的迭代器。因此,真正的调用堆栈被一系列对象层次结构替换,每次通过对下一个内部对象调用MoveNext()来重新创建正确的调用堆栈。


顺便提一下,当您调试异步时,在断点上看到的方法名称是MoveNext。这表明他们使用了IEnumerable中的迭代器状态机生成器。 - Luiz Felipe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接