C# 5.0中的新异步特性如何使用call/cc实现?

21

我一直在关注有关c# 5.0中将要推出的新async功能的最新公告。我对延续传递风格有基本的理解,并知道新的c#编译器对以下代码片段所进行的转换,该片段来自Eric Lippert的文章

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)原生实现了continuations,但我不太理解它是如何工作的,也不知道它确切地做了什么。

所以问题来了:如果Anders等人决定在C# 5.0中直接实现callcc而不是用 async/await特例,那么上面的代码段会是什么样子?

3个回答

29

原始回答:

据我理解,您的问题是:“如果不是为了实现基于任务的异步性而专门实现'await',而是实现了更一般的控制流操作'call-with-current-continuation',那会怎样?”

首先,让我们考虑一下"await"的作用。 "await" 接受类型为 Task<T> 的表达式,获取一个awaiter,并使用当前 continuation 调用 awaiter:

await FooAsync()

变得有效

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

现在假设我们有一个名为callcc的操作符,它以一个方法作为参数,并使用当前的继续来调用该方法。代码如下:

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

换句话说:

await FooAsync()

不过是

callcc FooAsync().GetAwaiter().BeginAwait;

那回答了你的问题吗?


更新 #1:

正如评论者所指出的,下面的答案假定从“技术预览”版本的async/await功能中生成代码模式。我们实际上在该功能的beta版中生成略有不同的代码,但逻辑上是相同的。目前的代码生成大致如下:

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:

正如Timwi在这个答案(https://dev59.com/Y2kw5IYBdhLWcg3wirCs#9826822)中指出的那样,“call/cc”和“await”的语义并不完全相同;一个“真正”的call/cc要求我们捕获方法的整个续体,包括其整个调用堆栈,或等价地,整个程序被重写成续传风格。

“await”功能更像是一种“协作式的call/cc”;续体仅捕获“当前任务返回方法即将在await点做什么?” 如果调用任务返回方法的呼叫者在任务完成后要执行一些有趣的动作,那么它可以自由地将其续体注册为任务的续体。


2
Eric,我总是惊叹于你似乎神奇地“发现”这些问题并在短时间内回答它们...是否有一个隐藏的“eric-lippert”标签会自动通知你?;) - Thomas Levesque
我想我终于明白了。await 帮助实现了 CPS(continuation passing style)风格。即使将整个程序重写为 CPS 风格,语义上仍与 call/cc 有很大不同:你仍然无法获得一个对象(即 continuation),通过它传递参数来返回原始 call/cc 调用的结果。 - Roman Starkov

4
我不是continuations方面的专家,但我会尝试解释async/await和call/cc之间的区别。当然,这个解释假设我理解call/cc和async/await,但我不确定我是否理解。无论如何,以下是解释:
使用C#中的“async”,您告诉编译器生成该特定方法的特殊版本,该版本了解如何将其状态装入堆数据结构中,以便可以将其“从真实堆栈”中删除并稍后恢复。在async上下文中,“await”就像“call/cc”,它使用编译器生成的对象来封装状态并脱离“真实堆栈”,直到任务完成。但是,由于它是编译器重写异步方法允许状态被封装,因此await只能在异步上下文中使用。
在一流的call/cc中,语言运行时生成所有代码,以便当前续体可以被装入一个call-continuation-function(使async关键字不必要)。call/cc仍然像await一样,导致当前续态(想象栈的状态)被封装并作为函数传递给调用函数。一种方法是对于所有函数调用使用堆帧,而不是“栈”帧。(有时称为'无栈',如'无栈python'或许多scheme实现)另一种方法是在调用call/cc目标之前从“真正的栈”中删除所有数据,并将其塞入堆数据结构中。
如果在堆栈上交错地调用外部函数(考虑DllImport),可能会出现一些棘手的问题。我怀疑这就是他们选择async/await实现的原因。

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


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

3

我觉得这有点毫无意义。Scheme解释器经常使用状态机实现call/cc,将本地状态捕获到堆上。这正是C# 5.0(或更正确地说是C# 2.0的迭代器)所做的。他们确实实现了call/cc,他们提出的抽象相当优雅。


重点是理解continuations。你的意思是await就是call/cc吗? - Gabe Moothart
1
是的,它的实现方式完全相同。使用ildasm或Reflector研究迭代器生成的IL以了解其工作原理。请注意类中神秘名称的状态机。 - Hans Passant
虽然它的实现方式相同,但 await 并不等同于 call/cc。实际上,它只是一个非常小的子集。重要的区别在于 call/cc 允许你从调用中传递参数到 continuation 中。请参见我的回答,Eric Lippert 在回应后也编辑了他的回答 - Timwi

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