【问题标题】:C# await vs continuations: not quite the same?C# 等待与延续:不完全一样?
【发布时间】:2012-04-07 06:22:16
【问题描述】:

阅读Eric Lippert’s answer 后,我的印象是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

至关重要的是,当调用 continuation 时,程序状态会随着调用历史恢复,因此 foo 返回到 main 并且永远不会返回到 bar。 p>

但是,如果在 C# 中使用 await 实现,则调用延续不会恢复此调用历史记录。 foo 返回到 bar,并且没有任何方法(我可以看到)await 可用于使正确的通话记录成为延续的一部分。

请解释一下:我完全误解了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”永远不会被调用,不像在 true-call/cc 示例中,这是喜欢 C# 并赞扬其设计的另一个原因!

【问题讨论】:

  • Timwi 是正确的; await 更像是一个“本地效果”调用/cc;这是我最初的答案中没有想到的一个微妙之处。我已经更新了。

标签: c# continuation


【解决方案1】:

await确实和call/cc不太一样。

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

假设您有一个包含等待表达式的异步函数:

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() 并在that 上调用MoveNext()。因此,真正的调用堆栈被对象层次结构所取代,这些对象层次结构每次通过对下一个内部对象上的MoveNext() 的一系列调用来重新创建正确的调用堆栈。

【讨论】:

  • 顺便说一下,当您调试异步时,您会看到 MoveNext 作为断点上的方法名称。这表明他们使用了 IEnumerable 的迭代器状态机生成器。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-19
相关资源
最近更新 更多