21

我一直在关注有关asyncc# 5.0 中新功能的新公告。我对延续传递风格和新的 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) 原生地实现了延续,但我并不真正理解它是如何工作的或它究竟做了什么。

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

4

3 回答 3

29

原答案:

据我了解,您的问题是“如果不是专门为基于任务的异步实现“等待”,而是实现了更通用的 call-with-current-continuation 控制流操作,该怎么办?

好吧,首先让我们想想“等待”是做什么的。"await" 接受一个类型的表达式Task<T>,获得一个等待者,并用当前的延续调用等待者:

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:

作为这个答案

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

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

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

于 2010-11-01T16:42:52.327 回答
4

我不是 continuation 方面的专家,但我会尝试解释 async/await 和 call/cc 之间的区别。当然,这个解释假设我理解 call/cc 和 async/await,我不确定我是否理解。尽管如此,这里...

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

在一流的 call/cc 中,语言运行时会生成所有代码,以便可以将当前的延续装瓶到 call-continuation-function 中(使 async 关键字变得不必要)。call/cc 仍然像等待一样,导致当前继续状态(想想堆栈状态)被打包并作为函数传递给被调用函数。一种方法是对所有函数调用使用堆帧,而不是“堆栈”帧。(有时称为“stackless”,如“stackless python”或许多方案实现)另一种方法是在调用 call/cc 目标之前从“真实堆栈”中删除所有数据并将其填充到堆数据结构中.

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

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


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

于 2010-12-16T17:36:20.490 回答
3

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

于 2010-11-01T15:43:24.963 回答