C#async/await如何与更一般的结构相关,例如F#工作流或monad?

Lor*_*tté 35 c# monads f# async-await

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

我们本来可以做得更普遍.我们的迭代器块可以看作是一种弱的协程.我们本可以选择实现完整的协同程序,并且只是使迭代器块成为协程的特例.当然,协同程序反过来不如一流的延续; 我们可以实现continuation,在continuation方面实现协程,并在协程方面实现迭代器.

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

C#类型系统的功能不足以为monad创建通用抽象,这是创建扩展方法和"查询模式"的主要动力.

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

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

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

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

Tom*_*cek 38

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

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

Monadic行动. C#异步基本上由两个基本操作组成.您可以await进行异步计算,并且可以return从异步计算得到结果(在第一种情况下,这是使用新关键字完成的,而在第二种情况下,我们将重新使用已在语言中的关键字).

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

Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);
Run Code Online (Sandbox Code Playgroud)

它们都可以使用标准的API任务很容易实现-第一个是基本的组合ContinueWithUnwrap,第二个简单的创建将立即返回值的任务.我将使用上述两个操作,因为它们更好地捕获了这个想法.

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

让我们看一下我们使用表达式e然后将结果赋值给变量x并计算表达式(或语句块)的情况body(在C#中,您可以等待内部表达式,但是您总是可以将其转换为首先将结果分配给一个变量):

[| var x = await e; body |] 
   = Bind(e, x => [| body |])
Run Code Online (Sandbox Code Playgroud)

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

在上面的例子中,当你有一个表达式时await e,它被转换为Bind操作,并且正文(等待之后的其余代码)被推送到作为第二个参数传递给的lambda函数Bind.

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

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

[| return e |]
   = Return(e)
Run Code Online (Sandbox Code Playgroud)

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

更大的例子.如果你看一个包含多个awaits 的更大的例子:

var x = await AsyncOperation();
return await x.AnotherAsyncOperation();
Run Code Online (Sandbox Code Playgroud)

代码将被翻译成以下内容:

Bind(AsyncOperation(), x =>
  Bind(x.AnotherAsyncOperation(), temp =>
    Return(temp));
Run Code Online (Sandbox Code Playgroud)

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

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

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

Async<T> = Action<Action<T>> 
Run Code Online (Sandbox Code Playgroud)

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


Eri*_*ert 32

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

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

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

毫无疑问,一端有"非常具体" 的频谱,另一端有"非常一般"的频谱,特定问题的解决方案属于该频谱.C#是一个整体设计,是解决许多特定问题的高度通用解决方案; 这就是通用编程语言.您可以使用C#编写从Web服务到XBOX 360游戏的所有内容.

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

设计的诀窍在于确定停止在哪个频谱上是有意义的.设计团队本来可以说,查询理解问题实际上只是绑定monad更一般问题的一个特例.绑定monad问题实际上只是在更高类型的类型上定义操作的更一般问题的特定情况.当然,对类型系统有一些抽象......足够了.当我们解决bind-an-arbitrary-monad问题时,解决方案现在如此普遍,以至于首先成为该功能动机的业务线程序员完全丢失了,我们避开了实际上解决了他们的问题.

自C#1.0以来添加的真正主要功能 - 泛型类型,匿名函数,迭代器块,LINQ,动态,异步 - 都具有以下特性:它们是在许多不同域中有用的高度通用特性.它们都可以被视为更普遍问题的具体例子,但对于任何问题的任何解决方案都是如此; 你可以随时使它变得更加通用.设计这些功能的想法是找到不使用户混淆的情况下不能更普遍的点.

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

我想要了解的是async/await关键字所涉及的"一般构造"

这取决于你怎么看了.

async-await功能是围绕Task<T>类型构建的,就像你注意到的那样,monad.当然,如果你和Erik Meijer谈到这一点,他会立即指出这Task<T>实际上是一个comonad ; 你可以T从另一端获得价值.

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

查看该功能的第三种方式是说它是一种带电流延续的呼叫(通常缩写为call/cc).它不是call/cc的完整实现,因为它在注册延续时没有采用调用堆栈的状态.有关详细信息,请参阅此问

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

我会等着看是否有人(Eric?Jon?也许你?)可以填写更多关于C#实际生成代码以实现等待的细节,

重写基本上只是重写迭代器块的变化.Mads在他的MSDN杂志文章中详细介绍了所有细节:

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