36

C#言語の設計は、根本的な一般的な問題に対処するのではなく、常に(歴史的に)特定の問題を解決することを目的としています。たとえば、http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/を参照してください。 iterator-blocks-part-one.aspx for "IEnumerable vs. coroutines":

もっと一般的にすることもできたでしょう。私たちのイテレータブロックは、弱い種類のコルーチンと見なすことができます。完全なコルーチンを実装することを選択し、イテレータブロックをコルーチンの特殊なケースにすることもできます。そしてもちろん、コルーチンは一流の継続よりも一般的ではありません。継続を実装し、継続の観点からコルーチンを実装し、コルーチンの観点からイテレータを実装することもできます。

またはhttp://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx(ある種の)モナドの代理としてのSelectManyの場合:

C#型システムは、拡張メソッドと「クエリパターン」を作成する主な動機となったモナドの一般化された抽象化を作成するのに十分なほど強力ではありません。

なぜそうなったのかを尋ねたくありません(特にEricのブログでは、コンパイラとプログラマーの両方のパフォーマンスから複雑さの増大まで、これらすべての設計上の決定に当てはまる可能性のある多くの良い答えがすでに与えられています)。

私が理解しようとしているのは、async / awaitキーワードがどの「一般的な構成」に関連しているかです(私の推測では、継続モナドです-結局のところ、F#非同期はワークフローを使用して実装されています。これは私の理解では継続モナドです)。それらがどのように関連しているか(どのように異なるのか、何が欠けているのか、なぜギャップがあるのか​​?)

リンクしたEricLippertの記事に似ていますが、IEnumerable/yieldではなくasync/awaitに関連する回答を探しています。

編集:すばらしい回答に加えて、関連する質問や提案されたブログ投稿へのいくつかの有用なリンク、私はそれらをリストするために私の質問を編集しています:

4

2 に答える 2

39

C#の非同期プログラミングモデルは、一般的なモナドパターンのインスタンスであるF#の非同期ワークフローと非常によく似ています。実際、C#イテレータ構文もこのパターンのインスタンスですが、追加の構造が必要なため、単純なモナドではありません。

これを説明することは、単一のSO回答の範囲をはるかに超えていますが、重要なアイデアについて説明します。

モナディック操作。 C#非同期は、基本的に2つの基本的な操作で構成されます。await非同期計算と非同期計算の結果を行うことができます(return最初のケースでは、これは新しいキーワードを使用して実行されますが、2番目のケースでは、すでに言語にあるキーワードを再利用しています)。

一般的なパターン(モナド)に従っている場合は、非同期コードを次の2つの操作の呼び出しに変換します。

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

これらは両方とも、標準のタスクAPIを使用して非常に簡単に実装できます。最初のAPIは基本的にとの組み合わせでContinueWithありUnwrap、2番目のAPIは値をすぐに返すタスクを作成するだけです。上記の2つの操作を使用します。これは、これらの操作の方がアイデアをよりよく捉えているためです。

翻訳。重要なことは、非同期コードを上記の操作を使用する通常のコードに変換することです。

e式を待ってから結果を変数に割り当て、x式(またはステートメントブロック)を評価する場合を見てみましょうbody(C#では、式の内部で待つことができますが、最初に結果を割り当てるコードにいつでも変換できます変数):

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

私はプログラミング言語で非常に一般的な表記法を使用しています。の意味は、式(「意味括弧」内)を他の式[| e |] = (...)に変換することです。e(...)

上記の場合、。を使用した式があるとawait e、それは操作に変換されBind、本体(awaitに続く残りのコード)は、2番目のパラメーターとしてに渡されるラムダ関数にプッシュされますBind

ここで面白いことが起こります!残りのコードをすぐに評価する(または待機中にスレッドをブロックする)代わりに、Bind操作は非同期操作(eタイプはTask<T>)を実行でき、操作が完了すると、最終的にラムダ関数を呼び出すことができます(継続)体の残りの部分を実行します。

変換の考え方は、あるタイプを返す通常のコードをR、値を非同期的に返すタスクに変換することです。つまり、ですTask<R>。上記の式では、の戻り型Bindは実際にタスクです。これが私たちが翻訳する必要がある理由でもありますreturn

[| return e |]
   = Return(e)

これは非常に簡単です。結果の値があり、それを返したい場合は、すぐに完了するタスクでラップするだけです。これは役に立たないように聞こえるかもしれませんが、操作(および翻訳全体)でaを返す必要があるTaskため、を返す必要があることを忘れないでください。Bind

より大きな例。await複数のを含むより大きな例を見ると、次のようになります。

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

コードは次のように変換されます。

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

重要なトリックはBind、コードの残りの部分を継続に変換することです(つまり、非同期操作が完了したときに評価できるということです)。

継続モナド。C#では、非同期メカニズムは実際には上記の変換を使用して実装されていません。その理由は、非同期のみに焦点を当てると、より効率的なコンパイル(C#が行うこと)を実行して、ステートマシンを直接生成できるためです。ただし、上記はF#で非同期ワークフローがどのように機能するかとほぼ同じです。Bindこれは、F#の追加の柔軟性の源でもあります-独自に定義できReturn、シーケンスの操作、ロギングの追跡、再開可能な計算の作成、さらには非同期計算とシーケンスの組み合わせなど、他のことを意味することができます(非同期シーケンスは複数の結果をもたらす可能性があります) 、しかし待つこともできます)。

F#の実装は継続モナドに基づいています。つまり、F#のTask<T>(実際にはAsync<T>)はおおよそ次のように定義されます。

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

つまり、非同期計算は何らかのアクションです。引数として(継続)をAction<T>指定すると、何らかの作業が開始され、最終的に終了すると、指定したこのアクションが呼び出されます。継続モナドを検索する場合は、C#とF#の両方でこれについてのより良い説明を見つけることができると確信しているので、ここで停止します...

于 2013-03-25T13:10:23.080 に答える
33

トーマスの答えはとても良いです。さらにいくつか追加するには:

C#言語の設計は、根本的な一般的な問題に対処することを見つけるのではなく、常に(歴史的に)特定の問題を解決することを目的としています。

それにはいくつかの真実がありますが、それが完全に公正または正確な特徴付けであるとは思わないので、あなたの質問の前提を否定することから私の答えを始めるつもりです。

確かに、一方の端に「非常に具体的」で、もう一方の端に「非常に一般的」なスペクトルがあり、特定の問題の解決策はそのスペクトルに当てはまります。C#は全体として、非常に多くの特定の問題に対する非常に一般的なソリューションとなるように設計されています。それが汎用プログラミング言語です。C#を使用して、WebサービスからXBOX360ゲームまですべてを作成できます。

C#は汎用プログラミング言語として設計されているため、設計チームが特定のユーザーの問題を特定するときは、常により一般的なケースを検討します。LINQはその好例です。LINQの設計のごく初期の頃は、SQLステートメントをC#プログラムに配置する方法にすぎませんでした。これは、特定された問題領域であるためです。しかし、設計プロセスのすぐに、チームは、データの並べ替え、フィルタリング、グループ化、結合の概念が、リレーショナルデータベースの表形式データだけでなく、XMLの階層データ、およびメモリ内のアドホックオブジェクトにも適用されることに気付きました。そして、彼らは私たちが今日持っているはるかに一般的な解決策を選ぶことにしました。

設計の秘訣は、スペクトルのどこで停止するのが理にかなっているのかを理解することです。設計チームは、まあ、クエリ理解の問題は、実際には、モナドをバインドするというより一般的な問題の特定のケースにすぎないと言ったかもしれません。そして、バインディングモナドの問題は、実際には、より高い種類の型に対する操作を定義するという、より一般的な問題の特定のケースにすぎません。そして確かに、型システムにはいくつかの抽象化があります...そして十分です。bind-an-arbitrary-monad問題を解決するまでに、解決策は非常に一般的になり、そもそもこの機能の動機であった基幹業務SQLプログラマーは完全に失われました。実際に彼らの問題を解決していません。

C#1.0以降に追加された本当に主要な機能(ジェネリック型、無名関数、イテレーターブロック、LINQ、動的、非同期)はすべて、多くの異なるドメインで役立つ非常に一般的な機能であるという特性を備えています。それらはすべて、より一般的な問題の具体例として扱うことができますが、それはあらゆる問題のあらゆる解決策に当てはまります。あなたはいつでもそれをより一般的にすることができます。これらの各機能の設計のアイデアは、ユーザーを混乱させることなく、より一般的にすることができないポイントを見つけることです。

私はあなたの質問の前提を否定したので、実際の質問を見てみましょう:

私が理解しようとしているのは、async/awaitキーワードがどの「一般的な構成」に関連しているかということです

それはあなたがそれをどのように見るかに依存します。

async-await機能はTask<T>、ご存知のようにモナドであるタイプを中心に構築されています。そしてもちろん、これについてErik Meijerと話した場合、彼はすぐにそれTask<T>が実際にはコモナドであると指摘します。Tもう一方の端から値を取り戻すことができます。

この機能を確認するもう1つの方法は、イテレータブロックについて引用した段落を使用して、「イテレータ」の代わりに「非同期」を使用することです。非同期メソッドは、イテレータメソッドと同様に、一種のコルーチンです。Task<T>必要に応じて、コルーチンメカニズムの実装の詳細と考えることができます。

この機能を確認する3番目の方法は、これが一種の現在の継続を伴う呼び出し(通常はcall / ccと略される)であると言うことです。継続がサインアップされたときのコールスタックの状態を考慮に入れていないため、call/ccの完全な実装ではありません。詳細については、次の質問を参照してください。

c#5.0の新しい非同期機能をcall / ccでどのように実装できますか?

私は待って、誰か(Eric?Jon?多分あなた?)が実際にC#がawaitを実装するためのコードを生成する方法の詳細を入力できるかどうかを確認します。

リライトは、基本的にイテレータブロックのリライト方法のバリエーションにすぎません。Madsは、MSDNMagazineの記事ですべての詳細を確認しています。

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

于 2013-03-25T15:20:23.753 に答える