首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >C#异步/等待如何与更一般的构造(例如F#工作流或monads )相关?

C#异步/等待如何与更一般的构造(例如F#工作流或monads )相关?
EN

Stack Overflow用户
提问于 2013-03-25 09:52:58
回答 2查看 3K关注 0票数 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作为(某种类型的)Monad的代理:

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

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

我想要了解的是,异步/等待关键字与哪个“一般构造”相关(我最好的猜测是延续monad -毕竟,F#异步是使用工作流实现的,据我理解,工作流是一个延续monad),以及它们与它之间的关系(它们有何不同?遗漏了什么?为什么会出现空白(如果有的话)?)

我正在寻找一个类似于我所链接的Eric文章的答案,但它与异步/等待相关,而不是IEnumerable/产。

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

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2013-03-25 13:10:23

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

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

一元操作。-- C#异步本质上包括两个基本操作。您可以对异步计算进行await,并且可以使用异步计算的结果(在第一种情况下,这是使用一个新关键字完成的,而在第二种情况下,我们正在重新使用已经在语言中的关键字)。

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

代码语言:javascript
运行
复制
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#中,您可以等待内部表达式,但可以将其转换为首先将结果分配给变量的代码):

代码语言:javascript
运行
复制
[| var x = await e; body |] 
   = Bind(e, x => [| body |])

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

在上述情况下,当您有一个带有await e的表达式时,它将被转换为Bind操作,主体(接下来的代码等待)被推入到一个lambda函数中,该函数作为第二个参数传递给Bind

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

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

代码语言:javascript
运行
复制
[| return e |]
   = Return(e)

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

大示例。如果您查看包含多个await的较大示例,则为

代码语言:javascript
运行
复制
var x = await AsyncOperation();
return await x.AnotherAsyncOperation();

代码将被翻译成如下所示:

代码语言:javascript
运行
复制
Bind(AsyncOperation(), x =>
  Bind(x.AnotherAsyncOperation(), temp =>
    Return(temp));

关键的诀窍是,每个Bind都将代码的其余部分转换为一个延续(这意味着可以在异步操作完成时对其进行评估)。

延续monad.在C#中,异步机制实际上没有使用上面的转换来实现。原因是,如果只关注异步,就可以进行更高效的编译(这就是C#所做的),并直接生成状态机。然而,上面的内容基本上就是异步工作流在F#中的工作方式。这也是F#中额外灵活性的来源--您可以将自己的BindReturn定义为其他东西--例如用于处理序列、跟踪日志记录、创建可恢复计算的操作,甚至将异步计算与序列相结合(异步序列可以产生多个结果,但也可以等待)。

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

代码语言:javascript
运行
复制
Async<T> = Action<Action<T>> 

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

票数 39
EN

Stack Overflow用户

发布于 2013-03-25 15:20:23

托马斯的回答很好。为了添加更多的内容:

C#语言设计(历史上)总是致力于解决特定的问题,而不是找到解决潜在的一般问题的方法。

虽然有一些事实,但我不认为这是一个完全公平或准确的定性,所以我要开始我的答案,否认你的问题的前提。

的确,一方面是“非常具体的”,另一方面是“非常笼统的”,而具体问题的解决办法则在这一范围内。C#作为一个整体被设计成一个解决许多具体问题的高度通用的解决方案;这就是通用编程语言的特性。您可以使用C#编写从web服务到XBOX 360游戏的所有内容。

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

设计的诀窍是找出在频谱上停止的意义所在。设计团队本来可以说,查询理解问题实际上只是绑定monads这一更为普遍的问题的一个具体案例。绑定单点问题实际上只是定义更高类型类型的操作这一更为普遍的问题的一个具体案例。当然,对类型系统有一些抽象.够了就够了。当我们解决绑定和任意的单一问题时,这个解决方案是如此的普遍,以至于最初支持这个特性的业务SQL程序员已经完全丧失了,而且我们还没有真正解决他们的问题。

自C# 1.0以来添加的真正主要的特性--泛型类型、匿名函数、迭代器块、LINQ、动态的、异步的--都具有这样的特性,即它们在许多不同的领域都是非常通用的特性。它们都可以被看作是一个更一般问题的具体例子,但是对于任何问题的解决方案都是如此;您总是可以使它更加通用。每个特性的设计思想都是为了找出这样一个点,即在不混淆用户的情况下,它们不能变得更通用。

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

我想要了解的是,异步/等待关键字与哪个“一般构造”相关。

这取决于你如何看待它。

异步等待功能是围绕Task<T>类型构建的,正如您注意到的那样,这是一个单一类型。当然,如果您和Erik讨论这个问题,他会立即指出Task<T>实际上是一个comonad;您可以从另一端得到T值。

查看该特性的另一种方法是以您引用的有关iterator块的段落代替"iterator“。异步方法和迭代器方法一样,是一种协同线。如果您愿意,可以将Task<T>看作是协同机制的实现细节。

第三种看待这个特性的方法是说它是一种带有当前延续的调用(通常缩写为call/cc)。它不是call/cc的完全实现,因为它没有考虑到连续注册时调用堆栈的状态。有关详情,请参阅本问题:

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

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

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

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

票数 33
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/15611972

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档