如何使用call / cc实现c#5.0中的新异步功能?

时间:2010-11-01 15:20:28

标签: c# asynchronous callcc async-await continuation-passing

我一直在关注将在c#5.0中使用的新async功能的新公告。我对继续传递样式和新c#编译器对代码的转换有了基本的了解,就像来自Eric Lippert's post的代码片段一样:

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);
  }
}

我知道有些语言通过call-with-current-continuation(callcc)原生实现continuation,但我真的不明白它是如何工作的,或者它是如何工作的。

所以这就是问题:如果安德斯等人。已经决定咬紧牙关,只是在c#5.0而不是callcc / async特殊情况下实施await,上面的代码会是什么样的?

3 个答案:

答案 0 :(得分:28)

原始答案:

正如我所理解的那样,你的问题是“如果不是实现”等待“专门用于基于任务的异步,而是实现了使用电流继续调用的更一般的控制流操作?”< / p>

嗯,首先让我们考虑一下“等待”的作用。 “await”采用类型Task<T>的表达式,获取awaiter,并使用当前的continuation调用awaiter:

await FooAsync()

变得有效

var task = FooAsync();
var awaiter = task.GetAwaiter();
awaiter.BeginAwait(somehow get the current continuation);

现在假设我们有一个运算符callcc,它将一个方法作为参数,并使用当前的continuation调用该方法。这看起来像这样:

var task = FooAsync();
var awaiter = task.GetAwaiter();
callcc awaiter.BeginAwait;

换句话说:

await FooAsync()

只不过是

callcc FooAsync().GetAwaiter().BeginAwait;

这会回答你的问题吗?


更新#1:

正如评论者所指出的,下面的答案假设来自async / await功能的“技术预览”版本的代码生成模式。我们实际上在该功能的测试版中生成略有不同的代码,但逻辑上它是相同的。现在的codegen就像:

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.

请注意,这有点复杂,并处理您“等待”已经计算过的结果的情况。如果您正在等待的结果实际上已经在内存中缓存,那么就没有必要通过所有对控制者产生控制的严格控制并再次接收。

因此,“await”和“callcc”之间的连接并不像预览版本那样简单,但很明显我们基本上在awaiter的“OnCompleted”方法上执行callcc。如果我们不需要,我们就不会使用callcc。


更新#2:

作为这个答案

https://stackoverflow.com/a/9826822/88656

来自Timwi的指出,call / cc和await的语义并不完全相同;一个“真正的”调用/ cc要求我们“捕获”方法整个延续,包括整个调用堆栈,或等效地将整个程序重写为continuation传递风格。

“等待”功能更像是“合作呼叫/ cc”;继续只捕获“当前任务返回方法在await点接下来要做什么?”如果任务返回方法的调用者在任务完成后要做一些有趣的事情,那么可以自由地注册其继续作为任务的继续。

答案 1 :(得分:4)

我不是延续的专家,但我会尝试解释async / await和call / cc之间的区别。当然这个解释假设我理解call / cc和async / await,我不确定我这样做。不过,这里有......

使用C#'async',您告诉编译器生成该特定方法的特殊版本,该方法了解如何将其状态置于堆数据结构中,因此可以“从实际堆栈中删除”并且稍后恢复。在异步上下文中,“await”就像“call / cc”,因为它使用编译器生成的对象来填充状态并离开“真实堆栈”,直到任务完成。但是,因为编译器重写了允许状态被装瓶的异步方法,所以await只能在异步上下文中使用。

在第一类调用/ cc中,语言运行库生成所有代码,使得当前的延续可以装入一个call-continuation-function(不再需要async关键字)。 call / cc仍然像await一样,导致当前继续状态(想到堆栈状态)被淹没并作为函数传递给被调用函数。一种方法是使用堆帧来进行所有函数调用,而不是“堆栈”帧。 (有时称为'无堆栈',如'无堆栈python'或许多方案实现)另一种方法是从“真实堆栈”中删除所有数据并在调用call / cc目标之前将其填充到堆数据结构中。

如果在堆栈上混合调用外部函数(想想DllImport),则会出现一些棘手的问题。我怀疑这是他们使用async / await实现的原因。

http://www.madore.org/~david/computers/callcc.html


因为在C#中,为了使用这些机制,必须将函数标记为“async”,我想知道这个异步关键字是否会成为一种病毒,它会扩散到许多库中的许多函数中。如果发生这种情况他们最终可能会意识到他们应该在VM级别实现一流的调用/ cc,而不是基于编译器重写的异步模型。只有时间会给出答案。但是,它在当前C#环境中肯定是一个有用的工具。

答案 2 :(得分:3)

有点无意义我会说。 Scheme解释器通常使用状态机实现call / cc,该状态机将本地状态捕获到堆上。哪个完全 C#5.0(或更正确的C#2.0的迭代器)也是如此。他们 实现了call / cc,他们提出的抽象非常优雅。