118

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

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

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

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

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

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

これは、ガベージ コレクターがもたらしたパラダイム シフトに匹敵するものかもしれません。初期の GC では、自分のメモリを解放する方が確実に効率的でした。しかし、大衆は依然として自動収集を選択し、より安全で単純なコードを優先して、効率が低下する可能性があります (そして、それは間違いなくもはや真実ではありません)。多分これはここに当てはまりますか?すべての関数を非同期にするべきではないのはなぜですか?

4

4 に答える 4

137

初めまして、嬉しいお言葉ありがとうございます。それは本当に素晴らしい機能であり、私はそれに少しでも参加できたことをうれしく思います.

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

まあ、あなたは誇張しています。すべてのコードが非同期になっていません。2 つの「プレーンな」整数を足し合わせると、結果を待っているわけではありません。2 つの将来の整数を加算して 3 番目の将来の整数を取得すると、それは将来アクセスする整数であるためTask<int>、もちろん結果を待つことになります。

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

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

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

この種の変換とそれに続く最適化パスに対する 3 つのポイントを挙げさせてください。

最初のポイントは次のとおりです。C#/VB/F# の async は、本質的に継続渡しの制限された形式です。関数型言語コミュニティでは、継続渡しスタイルを多用するコードを最適化する方法を特定する方法を見つけるために、膨大な量の研究が行われてきました。コンパイラ チームは、"async" がデフォルトであり、非非同期メソッドを識別して非非同期化する必要がある世界で、非常によく似た問題を解決しなければならない可能性があります。C# チームは、未解決の研究問題に取り組むことにあまり関心がないため、これは大きな反論となります。

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

非同期では、副作用がいつ発生するかを判断するのがはるかに難しくなります。非同期の前に、あなたが言った場合:

M();
N();

and M()was void M() { Q(); R(); }、 and N()was void N() { S(); T(); }、 and が副作用RS生成する場合、R の副作用が S の副作用の前に発生することがわかります。しかし、もしそうならasync void M() { await Q(); R(); }、突然それは窓の外に出ます。R()が前後に発生するかどうかは保証されませんS()(もちろんM()が待機されている場合を除きます。ただし、もちろん、 が発生Taskするまで待機する必要はありませんN()。)

副作用が発生する順序がわからないというこの特性が、プログラム内のすべてのコードに適用されることを想像してみてください。基本的に、どの式がどの順序で評価されるかはもうわかりません。つまり、すべての式が参照透過的である必要があり、これは C# のような言語では困難です。

反対の 3 つ目のポイントは、「なぜ async がそれほど特別なのか?」と自問する必要があることです。すべての操作が実際には である必要があると主張する場合はTask<T>、「そうではない理由」という質問に答えることができる必要がありLazy<T>ます。または「なぜNullable<T>ですか?」または「なぜIEnumerable<T>ですか?」それは同じくらい簡単にできるからです。すべての操作が nullable に持ち上げられるべきではないのはなぜですか? または、すべての操作が遅延計算され、後で使用できるように結果がキャッシュされます。または、すべての操作の結果が、単一の値ではなく一連の値になります。次に、「ああ、これは null であってはならないので、より良いコードを生成できる」などの状況を最適化する必要があります。

Task<T>要点は、これほど多くの作業を正当化するのが実際にそれほど特別であることは私には明らかではありません。

これらの種類のことに興味がある場合は、Haskell などの関数型言語を調査することをお勧めします。これは、はるかに強力な参照透過性を持ち、あらゆる種類の順不同の評価を許可し、自動キャッシュを実行します。また、Haskell の型システムは、私が言及したような「モナディック リフティング」の種類をはるかに強力にサポートしています。

于 2013-08-28T23:19:09.330 に答える
4

拡張性が必要でない場合は、すべてのメソッドを非同期にする正当な理由があると思います。選択的なメソッドの非同期化は、コードが進化せず、メソッド A() が常に CPU バウンド (同期を維持する) であり、メソッド B() が常に I/O バウンド (非同期としてマークする) であることがわかっている場合にのみ機能します。

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

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

于 2015-03-02T13:51:24.057 に答える