36

C# 语言设计一直(历史上)旨在解决特定问题,而不是寻找解决潜在的一般问题:例如参见http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/ iterator-blocks-part-one.aspx用于“IEnumerable 与协程”:

我们本可以让它更通用。我们的迭代器块可以看作是一种弱协程。我们本可以选择实现完整的协程,只是让迭代器块成为协程的一个特例。当然,协程也没有一流的延续那么通用。我们可以实现延续,在延续方面实现协程,在协程方面实现迭代器。

http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx用于 SelectMany 作为(某种)Monads 的代理:

C# 类型系统不够强大,无法为 monad 创建通用抽象,而这是创建扩展方法和“查询模式”的主要动力

我不想问为什么会这样(已经给出了很多好的答案,尤其是在 Eric 的博客中,这可能适用于所有这些设计决策:从性能到增加的复杂性,对编译器和程序员都是如此)。

我想了解的是 async/await 关键字与哪个“一般构造”相关(我最好的猜测是 continuation monad - 毕竟,F# async 是使用工作流实现的,据我所知,这是一个 continuation monad),并且它们与它的关系(它们有何不同?,缺少什么?,为什么会有差距,如果有的话?)

我正在寻找与我链接的 Eric Lippert 文章类似的答案,但与 async/await 而不是 IEnumerable/yield 相关。

编辑:除了很好的答案,一些有用的链接到相关问题和建议的博客文章,我正在编辑我的问题以列出它们:

4

2 回答 2

39

C# 中的异步编程模型与 F# 中的异步工作流非常相似,后者是通用monad模式的一个实例。事实上,C# 迭代器语法也是这种模式的一个实例,虽然它需要一些额外的结构,所以它不仅仅是简单的monad。

解释这一点远远超出了单个 SO 答案的范围,但让我解释一下关键思想。

一元操作。 C# async 本质上由两个原始操作组成。您可以await进行异步计算,也可以return从异步计算中获得结果(在第一种情况下,这是使用 new 关键字完成的,而在第二种情况下,我们正在重新使用该语言中已经存在的关键字)。

如果您遵循一般模式(monad),那么您会将异步代码转换为对以下两个操作的调用:

Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);

它们都可以很容易地使用标准任务 API 实现——第一个本质上是ContinueWith和的组合,Unwrap第二个只是创建一个立即返回值的任务。我将使用上述两个操作,因为它们更好地抓住了这个想法。

翻译。关键是将异步代码转换为使用上述操作的普通代码。

让我们看一个例子,当我们等待一个表达式e,然后将结果分配给一个变量x并评估表达式(或语句块)body(在 C# 中,您可以在表达式内部等待,但您总是可以将其转换为首先将结果分配给的代码一个变量):

[| var x = await e; body |] 
   = Bind(e, x => [| body |])

我使用的符号在编程语言中很常见。的意思[| e |] = (...)是我们将表达式e(在“语义括号”中)翻译成其他表达式(...)

在上述情况下,当您有一个带有 的表达式时await e,它会被转换为Bind操作,并且主体(await 之后的其余代码)被推入一个 lambda 函数,该函数作为第二个参数传递给Bind.

这就是有趣的事情发生的地方!该操作可以运行异步操作(由其类型表示),而不是立即评估其余代码(或在等待时阻塞线程),并且当操作完成时,它最终可以调用 lambda 函数(继续)运行身体的其余部分。BindeTask<T>

翻译的想法是,它将返回某种类型的普通代码转换为R异步返回值的任务——即Task<R>. 在上面的等式中,返回类型Bind确实是一个任务。这也是我们需要翻译的原因return

[| return e |]
   = Return(e)

这很简单——当你有一个结果值并且你想返回它时,你只需将它包装在一个立即完成的任务中。这听起来可能没用,但请记住,我们需要返回 aTask因为Bind操作(以及我们的整个翻译)需要它。

更大的例子。如果您查看包含多个awaits 的更大示例:

var x = await AsyncOperation();
return await x.AnotherAsyncOperation();

代码将被翻译成这样的:

Bind(AsyncOperation(), x =>
  Bind(x.AnotherAsyncOperation(), temp =>
    Return(temp));

关键技巧是 everyBind将其余代码变成一个延续(这意味着它可以在异步操作完成时进行评估)。

继续单子。在 C# 中,异步机制实际上并没有使用上述翻译实现。原因是如果只关注异步,可以进行更高效的编译(C# 就是这样做的)并直接生成状态机。但是,以上几乎就是异步工作流在 F# 中的工作方式。这也是 F# 中额外灵活性的来源——您可以定义自己的内容BindReturn表示其他内容——例如处理序列的操作、跟踪日志记录、创建可恢复的计算,甚至将异步计算与序列结合起来(异步序列可以产生多个结果, 但也可以等待)。

F# 实现基于continuation monad,这意味着Task<T>(实际上,Async<T>)在 F# 中的定义大致如下:

Async<T> = Action<Action<T>> 

也就是说,异步计算是一些动作。当你给它Action<T>(一个延续)作为参数时,它会开始做一些工作,然后,当它最终完成时,它会调用你指定的这个动作。如果您搜索 continuation monads,那么我相信您可以在 C# 和 F# 中找到更好的解释,所以我会在这里停下来......

于 2013-03-25T13:10:23.080 回答
33

托马斯的回答非常好。添加更多内容:

C# 语言设计一直(历史上)旨在解决特定问题,而不是寻找解决潜在的一般问题

虽然这有一些道理,但我认为这不是一个完全公平或准确的描述,所以我将通过否认你的问题的前提来开始我的回答。

确实存在一个范围,一端是“非常具体”,另一端是“非常一般”,特定问题的解决方案属于该范围。C# 作为一个整体被设计为针对许多特定问题的高度通用的解决方案;这就是通用编程语言。您可以使用 C# 编写从 Web 服务到 XBOX 360 游戏的所有内容。

由于 C# 被设计为一种通用编程语言,因此当设计团队识别出特定的用户问题时,他们总是会考虑更一般的情况。LINQ 就是一个很好的例子。在 LINQ 设计的早期,它只不过是一种将 SQL 语句放入 C# 程序的方法,因为这是确定的问题空间。但在设计过程中,团队很快意识到排序、过滤、分组和连接数据的概念不仅适用于关系数据库中的表格数据,还适用于 XML 中的分层数据,以及内存中的临时对象。因此,他们决定采用我们今天拥有的更通用的解决方案。

设计的诀窍是找出在频谱上停止的地方是有意义的。设计团队可以说,查询理解问题实际上只是更一般的绑定 monad 问题的一个特例。绑定单子问题实际上只是在更高种类的类型上定义操作的更一般问题的一个特定情况。当然,类型系统有一些抽象......足够了。当我们开始解决 bind-an-arbitrary-monad 问题时,解决方案现在已经如此普遍,以至于最初推动该功能的业务线 SQL 程序员完全迷失了,我们没有并没有真正解决他们的问题。

自 C# 1.0 以来添加的真正主要特性——泛型类型、匿名函数、迭代器块、LINQ、动态、异步——都具有在许多不同领域中非常有用的特性。它们都可以被视为更一般问题的具体示例,但对于任何问题的任何解决方案都是如此;你总是可以让它更通用。这些特性中的每一个的设计理念是找到一个点,即它们不能被更通用化,而不会让用户感到困惑

既然我已经否认了你的问题的前提,让我们来看看实际的问题:

我想了解的是 async/await 关键字与哪个“一般构造”相关

这取决于你怎么看了。

async-await 功能是围绕Task<T>类型构建的,正如您所注意到的,monad。当然,如果你和 Erik Meijer 谈到这个,他会立即指出这Task<T>实际上是一个共胞;你可以T从另一端取回价值。

查看该功能的另一种方法是使用您引用的有关迭代器块的段落,并将“异步”替换为“迭代器”。与迭代器方法一样,异步方法是一种协程。如果您愿意,可以将其Task<T>视为协程机制的一个实现细节。

第三种看待特性的方式是说它是一种 call-with-current-continuation(通常缩写为 call/cc)。它不是 call/cc 的完整实现,因为它没有考虑注册延续时调用堆栈的状态。有关详细信息,请参阅此问题:

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

我会等着看是否有人(埃里克?乔恩?也许是你?)可以填写更多关于 C# 实际如何生成代码来实现等待的细节,

重写本质上只是迭代器块如何重写的一种变体。Mads 在他的 MSDN 杂志文章中详细介绍了所有细节:

http://msdn.microsoft.com/en-us/magazine/hh456403.aspx

于 2013-03-25T15:20:23.753 回答