如何C#异步/等待涉及到更一般的结构,例如F#工作流或单子? [英] How does C# async/await relates to more general constructs, e.g. F# workflows or monads?

查看:133
本文介绍了如何C#异步/等待涉及到更一般的结构,例如F#工作流或单子?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

C#语言的设计一直(历史)已经着眼于解决具体问题而不是寻求解决根本问题一般:例如,见<一href=\"http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx\">http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx为IEnumerable的协同程序与


  

我们可以使人们更加普遍。我们的迭代的块可以被看作是一个弱一种协程。我们可以选择实现全面协同程序,只是做迭代器块协同程序的一种特殊情况。当然,协同程序又比一流的延续普遍较少;我们可以实现的延续,在延续来实现的协同程序和迭代的协同程序的条款。


或<一个href=\"http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx\">http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx对于作为的SelectMany的(某种)单子替代:


  

C#的类型系统是没有强大到足以创造这对创建扩展方法的主要动力和查询模式单子广义抽象


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

我想了解是哪些一般结构异步/等待关键字涉及到(我最好的猜测是延续单子 - 毕竟,F#异步正在使用的工作流程,这在我的理解是延续单子实施),以及它们如何与它(它们之间的区别?缺什么?为什么还有一定的差距,如果有的话?)

我在寻找类似埃里克利珀文章我联系一个答案,而是异步/相关等待,而不是IEnumerable的/产量。


  

修改:除了伟大的答案,一些有用的链接到相关的问题,并建议在这里的博客文章,我编辑我的问题一一列举:


  
  

解决方案

在C#中的异步编程模型非常相似的异步工作流的F#中,这是一般的实例的单子的模式。在事实上,C#迭代语法也是这种模式的一个实例,尽管它需要一些额外的结构,所以它不只是<青霉>简单的的单子。

解释这远远超出一个范围SO回答,但让我解释一下关键思想。

单子操作。
C#的异步基本上由两个基本操作。您可以等待异步计算,您可以收益从异步计算(在第一种情况下的结果,这是用一个新的关键字,而在第二种情况下,我们重新使用的关键字已经在语言)来完成。

如果你是以下的一般模式(单子的),那么你会异步code到的调用翻译成下面两个步骤:

 任务&LT; R&GT;绑定&LT; T,R&GT;(任务&LT; T&GT;计算,Func键&LT; T,任务&LT; R&GT;&GT;延续);
任务&LT; T&GT;返回&lt; T&GT;(T值);

他们都可以使用标准的任务API很容易实现 - 第一个是基本 ContinueWith 的组合和展开,第二个简单的创建一个将立即返回值的任务。我将使用上述两种操作,因为他们更好地捕捉想法。

翻译。的关键是翻译的异步code 的到使用上述操作正常code。

让我们看一个案例,当我们avait一个前pression 电子,然后将结果分配给一个变​​量 X 和评估前pression(或语句块)(在C#中,你可以等待里面的前pression,但你总是那意思就是code,第一批的结果分配给一个变​​量):

  [| VAR X =等待Ë;身体|]
   =绑定(E,X =&GT; |身体|])

我使用的符号,这是比较常见的编程语言。 的含义[| E |] =(...)是我们翻译的前pression 电子(在语义括号),其他一些前pression (...)

在上述情况下,当你有一个前pression与等待è,它被翻译成绑定操作和主体(code下面等待的其余部分)被压入被作为第二个参数传递给绑定

这是有趣的事情发生在哪里!相反,评估code其余的立即的(或阻塞线程在等待),在绑定操作可以运行的异步操作(中再由电子这类型的任务&LT psented $ p $; T&GT; ),而当操作完成后,它终于可以调用lambda函数(续)跑尸体的其余部分。

翻译的想法是,它变成普通code,返回某种类型的研究来异步返回值的任务 - 那就是任务&LT; R&GT; 。在上述公式中,绑定的返回类型,着实是一个任务。这也是为什么我们需要翻译收益

  [|返回E |]
   =回报(E)

这是很简单 - 当你有一个结果值,你想退货,你只需把它包在立即完成的任务。这听起来毫无用处,但请记住,我们需要返回一个工作因为绑定操作(和我们的整个翻译)要求。

更大的示例如果你看一下,它包含多个的更大的示例的await S:

  VAR X =等待AsyncOperation();
返回等待x.AnotherAsyncOperation();

在code将被转换为这样的事:

 绑定(AsyncOperation(),X =&GT;
  绑定(x.AnotherAsyncOperation(),温度=&GT;
    返程(TEMP));

关键诀窍是每一个绑定轮番code其余为延续(这意味着它可以在完成异步操作进行评估)。

延续单子。在C#中,异步机制实际上并没有使用上述转换实现的。其原因是,如果你只关注异步,你可以做一个更高效的编译(这是C#一样),并直接产生一个状态机。然而,上面是pretty异步工作流在F#中多是如何工作的。这也是在F#中附加的灵活性源 - 你可以定义自己的绑定返回意味着其他的事情 - 如与序列工作,跟踪记录,创造可恢复计算,甚至有序列结合异步计算(异步序列可以产生多个结果,也可以等待)。

操作

F#的实现是基于的延续单子的,这意味着任务&LT; T&GT; (实际上,异步&LT; T&GT; )的F#是一个粗略的定义是这样的:

 异步&LT; T&GT; =动作&lt;作用&LT; T&GT;&GT;

也就是说,异步计算是一些动作。当你给它动作&LT; T&GT; (延续)作为参数,它会开始做一些工作,然后,当它最终完成后,它会调用你指定的这个动作。如果您搜索延续单子,那么我敢肯定,你可以在C#和F#的这个更好的解释,所以我会在这里停...

The C# language design have always (historically) been geared towards solving specific problems rather then finding to address the underlying general problems: see for example http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx for "IEnumerable vs. coroutines":

We could have made it much more general. Our iterator blocks can be seen as a weak kind of coroutine. We could have chosen to implement full coroutines and just made iterator blocks a special case of coroutines. And of course, coroutines are in turn less general than first-class continuations; we could have implemented continuations, implemented coroutines in terms of continuations, and iterators in terms of coroutines.

or http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx for SelectMany as a surrogate for (some kind of) Monads:

The C# type system is not powerful enough to create a generalized abstraction for monads which was the primary motivator for creating extension methods and the "query pattern"

I do not want to ask why has been so (many good answers have been already given, especially in Eric's blog, which may apply to all these design decisions: from performance to increased complexity, both for the compiler and the programmer).

What I am trying to understand is to which "general construct" the async/await keywords relate to (my best guess is the continuation monad - after all, F# async is implemented using workflows, which to my understanding is a continuation monad), and how they relate to it (how they differ?, what is missing?, why there is a gap, if any?)

I'm looking for an answer similar to the Eric Lippert article I linked, but related to async/await instead of IEnumerable/yield.

Edit: besides the great answers, some useful links to related questions and blog posts where suggested, I'm editing my question to list them:

解决方案

The asynchronous programming model in C# is very similar to asynchronous workflows in F#, which are an instance of the general monad pattern. In fact, the C# iterator syntax is also an instance of this pattern, although it needs some additional structure, so it is not just simple monad.

Explaining this is well beyond the scope of a single SO answer, but let me explain the key ideas.

Monadic operations. The C# async essentially consists of two primitive operations. You can await an asynchronous computation and you can return the result from an asynchronous computation (in the first case, this is done using a new keyword, while in the second case, we're re-using a keyword that is already in the language).

If you were following the general pattern (monad) then you would translate the asynchronous code into calls to the following two operations:

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

They can both be quite easily implemented using the standard task API - the first one is essentially a combination of ContinueWith and Unwrap and the second one simply creates a task that returns the value immediately. I'm going to use the above two operations, because they better capture the idea.

Translation. The key thing is to translate asynchronous code to normal code that uses the above operations.

Let's look at a case when we avait an expression e and then assign the result to a variable x and evaluate expression (or statement block) body (in C#, you can await inside expression, but you could always translate that to code that first assigns the result to a variable):

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

I'm using a notation that is quite common in programming languages. The meaning of [| e |] = (...) is that we translate the expression e (in "semantic brackets") to some other expression (...).

In the above case, when you have an expression with await e, it is translated to the Bind operation and the body (the rest of the code following await) is pushed into a lambda function that is passed as a second parameter to Bind.

This is where the interesting thing happens! Instead of evaluating the rest of the code immediately (or blocking a thread while waiting), the Bind operation can run the asynchronous operation (represented by e which is of type Task<T>) and, when the operation completes, it can finally invoke the lambda function (continuation) to run the rest of the body.

The idea of the translation is that it turns ordinary code that returns some type R to a task that returns the value asynchronously - that is Task<R>. In the above equation, the return type of Bind is, indeed, a task. This is also why we need to translate return:

[| return e |]
   = Return(e)

This is quite simple - when you have a resulting value and you want to return it, you simply wrap it in a task that immediately completes. This might sound useless, but remember that we need to return a Task because the Bind operation (and our entire translation) requires that.

Larger example. If you look at a larger example that contains multiple awaits:

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

The code would be translated to something like this:

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

The key trick is that every Bind turns the rest of the code into a continuation (meaning that it can be evaluated when an asynchronous operation is completed).

Continuation monad. In C#, the async mechanism is not actually implemented using the above translation. The reason is that if you focus just on async, you can do a more efficient compilation (which is what C# does) and produce a state machine directly. However, the above is pretty much how asynchronous workflows work in F#. This is also the source of additional flexibility in F# - you can define your own Bind and Return to mean other things - such as operations for working with sequences, tracking logging, creating resumable computations or even combining asynchronous computations with sequences (async sequence can yield multiple results, but can also await).

The F# implementation is based on the continuation monad which means that Task<T> (actually, Async<T>) in F# is defined roughly like this:

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

That is, an asynchronous computation is some action. When you give it Action<T> (a continuation) as an argument, it will start doing some work and then, when it eventually finishes, it invokes this action that you specified. If you search for continuation monads, then I'm sure you can find better explanation of this in both C# and F#, so I'll stop here...

这篇关于如何C#异步/等待涉及到更一般的结构,例如F#工作流或单子?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆