繁体   English   中英

C# async/await 如何与更通用的构造(例如 F# 工作流或 monad)相关?

[英]How does C# async/await relates to more general constructs, e.g. F# workflows or monads?

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

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

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

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

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

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

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

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

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

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

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

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

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

它们都可以使用标准任务 API 轻松实现——第一个本质上是ContinueWithUnwrap的组合,第二个只是创建一个立即返回值的任务。 我将使用上述两个操作,因为它们更好地捕捉了这个想法。

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

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

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

我正在使用一种在编程语言中很常见的符号。 [| e |] = (...)的含义 [| e |] = (...)是我们将表达式e (在“语义括号”中)转换为其他表达式(...)

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

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

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

[| return e |]
   = Return(e)

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

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

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

代码将被翻译成这样:

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

关键技巧是每个Bind将其余代码转换为延续(意味着可以在异步操作完成时对其进行评估)。

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

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

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

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

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

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>实际上是一个comonad 你可以从另一端取回T值。

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM