web-dev-qa-db-ja.com

デフォルトですべての機能を非同期にしないのはなぜですか?

.net 4.5の async-await パターンはパラダイム変更です。それは本当であるにはあまりにも良いです。

ブロッキングは過去のものであるため、私はIOが重いコードをasync-awaitに移植しています。

かなり少数の人々がasync-awaitをゾンビのfest延と比較しており、かなり正確であることがわかりました。非同期コードは他の非同期コードと同様です(非同期関数を待つには非同期関数が必要です)。そのため、ますます多くの関数が非同期になり、これがコードベースで増え続けています。

関数を非同期に変更することは、幾分反復的で想像力に欠ける作業です。宣言でasyncキーワードをスローし、戻り値をTask<>でラップすると、ほぼ完了です。全体のプロセスがどれほど簡単かは不安であり、すぐにテキスト置換スクリプトが私の「移植」の大部分を自動化してくれます。

そして今、質問..すべてのコードがゆっくりと非同期になっている場合、デフォルトですべてを非同期にしないのはなぜですか?

私が推測する明白な理由はパフォーマンスです。 Async-awaitにはオーバーヘッドがあり、コードは非同期である必要はありませんが、非同期であることが望ましいです。しかし、パフォーマンスが唯一の問題である場合、いくつかの巧妙な最適化により、不要なオーバーヘッドを自動的に削除できます。 "高速パス" 最適化について読んだことがありますが、それだけで大部分を処理できるように思えます。

たぶんこれは、ガベージコレクターによってもたらされるパラダイムシフトに匹敵します。 GCの初期の頃は、独自のメモリを解放する方が効率的でした。しかし、大衆は依然として、より効率的ではない可能性のある、より安全でシンプルなコードを支持して自動コレクションを選択しました(そして、それは間違いなくもはや真実ではありません)。多分これはここに当てはまるでしょうか?すべての機能を非同期にしないのはなぜですか?

97
talkol

最初に、親切な言葉をありがとう。それは確かに素晴らしい機能であり、私はその一部であったことを嬉しく思います。

すべてのコードがゆっくりと非同期になっている場合、デフォルトですべてを非同期にしないのはなぜですか?

さて、あなたは誇張しています。 allコードは非同期になりません。 2つの「プレーン」整数を加算すると、結果を待っていません。 2つのfuture integersを追加して3番目のfuture integerを取得すると、それが_Task<int>_であるため、将来アクセスする整数-もちろん、結果を待つことになるでしょう。

すべてを非同期にしない主な理由は、async/awaitの目的は、多くの高遅延操作のある世界でコードを記述しやすくすることですであるためです。あなたの操作の大部分は高遅延ではないので、その遅延を軽減するパフォーマンスヒットを取ることは意味がありません。むしろ、重要なごく少数の操作が高レイテンシであり、それらの操作がコード全体でゾンビの非同期攻撃を引き起こしています。

パフォーマンスが唯一の問題である場合、確かにいくつかの巧妙な最適化により、不要なオーバーヘッドを自動的に削除できます。

理論的には、理論と実践は似ています。実際には、そうではありません。

最適化パスが後に続くこの種の変換に対する3つのポイントを挙げましょう。

繰り返しますが、C#/ VB/F#の非同期は、本質的に継続渡しの制限された形式です。関数型言語コミュニティでの膨大な量の研究は、継続パッシングスタイルを多用するコードを最適化する方法を特定する方法を見つけ出しました。コンパイラチームは、「非同期」がデフォルトであり、非同期でないメソッドを特定し、非同期化を解除する必要がある世界では、非常によく似た問題を解決する必要があります。 C#チームは、オープンな研究問題に取り組むことにあまり関心がないので、そこに大きな反対点があります。

反対の2番目の点は、C#には、この種の最適化をより扱いやすくする「参照透過性」のレベルがないことです。 「参照透過性」とは、式の値がいつ評価されるかに依存しないプロパティを意味します。 _2 + 2_のような式は参照的に透過的です。必要に応じて、コンパイル時に評価を行うか、実行時まで延期して同じ答えを得ることができます。ただし、xとyは時間とともに変化する可能性があるため、_x+y_のような式は時間内に移動できません

非同期は、副作用がいつ発生するかを推論することをはるかに困難にします。非同期の前に、あなたが言ったなら:

_M();
N();
_

およびM()void M() { Q(); R(); }であり、N()void N() { S(); T(); }であり、RおよびS生産側効果があれば、Sの副作用の前にRの副作用が発生することがわかります。しかし、async void M() { await Q(); R(); }があると、突然ウィンドウの外に出ます。 R()S()の前に発生するか後に発生するかは保証されません(もちろんM()が待機している場合を除きますが、もちろんTaskN()の後まで待つ必要はありません。)

オプティマイザが非同期化解除を管理するものを除いて、この順序の副作用が発生する順序がわからないプログラム内のコードの各部分に適用されることを想像してください。基本的に、どの式がどの順序で評価されるのか、もう手掛かりがありません。つまり、すべての式は参照的に透明である必要があり、C#のような言語では困難です。

反対の3番目のポイントは、「なぜ非同期がそんなに特別なのか?」すべての操作が実際に_Task<T>_であると主張する場合は、「なぜ_Lazy<T>_ではないのか?」という質問に答える必要があります。または「なぜ_Nullable<T>_ではないのですか?」または「なぜ_IEnumerable<T>_ではないのですか?」簡単にできるからです。なぜすべての操作がnullableに引き上げられるのではないのですか?または、すべての操作が遅延計算され、結果は後でキャッシュされます、またはすべての操作の結果は、単一の値ではなく一連の値です。次に、「ああ、これは決してnullであってはならないので、より良いコードを生成できる」などの状況を最適化する必要があります。 (実際、C#コンパイラはリフト演算のためにそうします。)

重要なのは、_Task<T>_が実際にこれほど多くの作業を保証する特別なものであるかどうかは私には明らかではないということです。

これらの種類に興味がある場合は、Haskellのように、参照の透明性がはるかに高く、あらゆる種類の順不同の評価を可能にし、自動キャッシュを行う関数型言語を調査することをお勧めします。 Haskellはまた、私が示唆したような「単項リフティング」の種類に対して、その型システムではるかに強力なサポートを持っています。

113
Eric Lippert

すべての機能を非同期にしないのはなぜですか?

あなたが述べたように、パフォーマンスが理由の1つです。リンクした「高速パス」オプションは、タスクが完了した場合のパフォーマンスを改善しますが、単一のメソッド呼び出しと比較して、はるかに多くの命令とオーバーヘッドが必要になることに注意してください。そのため、「高速パス」が適切に設定されていても、非同期メソッドの呼び出しごとに多くの複雑さとオーバーヘッドが追加されます。

下位互換性および他の言語との互換性(相互運用シナリオを含む)も問題になります。

もう1つは、複雑さと意図の問題です。非同期操作は複雑さを増します-多くの場合、言語機能はこれを隠しますが、メソッドasyncを作成すると、その使用法が明らかに複雑になることがよくあります。これは、非同期メソッドが予期しないスレッドの問題を引き起こしやすいため、同期コンテキストがない場合に特に当てはまります。

さらに、本質的に非同期ではない多くのルーチンがあります。これらは同期操作としてより理にかなっています。強制Math.Sqrt することが Task<double> Math.SqrtAsyncは、たとえば、それが非同期である理由がまったくないため、ばかげています。 asyncをアプリケーションにプッシュする代わりに、awaiteverywhereを伝播することになります。

また、これは現在のパラダイムを完全に破壊するだけでなく、プロパティの問題を引き起こします(実際にはメソッドペアです。これらは非同期になりますか?)、フレームワークと言語の設計全体に他の影響を与えます。

多くのIOバインドされた作業を行っている場合、asyncを広く使用することは素晴らしい追加であることに気付くでしょう。あなたのルーチンの多くはasyncになります。一般に、CPUにバインドされた作業を行うことで、asyncを実際に作成するのは良くありません。APIの下でCPUサイクルを使用しているという事実を隠しています表示非同期ですが、実際には必ずしも本当に非同期ではありません。

21
Reed Copsey

パフォーマンスはさておき、非同期には生産性のコストがかかる可能性があります。クライアント(WinForms、WPF、Windows Phone)では、生産性が向上します。しかし、サーバー上、または他の非UIシナリオでは、支払い生産性です。もちろん、デフォルトでは非同期になりたくないでしょう。スケーラビリティの利点が必要な場合に使用します。

スイートスポットで使用します。他の場合には、しないでください。

3
usr

拡張が必要でない場合、すべてのメソッドを非同期にする正当な理由があると思います。選択的非同期メソッドは、コードが進化せず、method A()が常にCPUにバインドされている(同期を維持)およびmethod B()は常にI/Oバウンドです(非同期とマークします)。

しかし、物事が変化したらどうなりますか?はい、A()は計算を行っていますが、将来のある時点で、そこにロギング、レポート、または予測できない実装を伴うユーザー定義のコールバックを追加する必要がありました。拡張され、CPU計算だけでなくI/Oも含まれるようになりましたか?メソッドを非同期に変換する必要がありますが、これによりAPIが破損し、スタック上のすべての呼び出し元も更新する必要があります(さらに、または、異なるバージョンの異なるアプリである)。または、同期バージョンと一緒に非同期バージョンを追加する必要がありますが、これはそれほど違いはありません-同期バージョンを使用するとブロックされるため、ほとんど受け入れられません。

APIを変更せずに既存の同期メソッドを非同期にすることができれば素晴らしいと思います。しかし、実際にはそのようなオプションはありません。現在必要ではない場合でも非同期バージョンを使用することが、将来互換性の問題が発生しないことを保証する唯一の方法です。

2
Alex