デフォルトですべての関数を非同期にする必要がないのはなぜですか?


107

.net 4.5 の非同期待機パターンは、パラダイムの変化です。それは本当であるにはほとんど良すぎる。

ブロッキングが過去のものであるため、いくつかのIO負荷の高いコードをasync-awaitに移植しています。

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

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

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

私が仮定する明白な理由はパフォーマンスです。Async-awaitにはオーバーヘッドがあり、非同期である必要のないコードがあり、できればそうするべきではありません。ただし、パフォーマンスのみが問題である場合、いくつかの巧妙な最適化によって、オーバーヘッドが不要になったときに自動的にオーバーヘッドを取り除くことができます。「ファストパス」の最適化について読みましたが、それだけでほとんどの問題を解決できるようです。

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


8
C#チームにマップをマークするためのクレジットを与えます。何百年も前に行われたように、「ドラゴンはここにいる」。船に装備してそこに行くこともできますが、太陽が照り、風が背中にあっても、それを乗り切ることができるでしょう。そして、時には、彼らは二度と戻ってこなかった。async / awaitと同様に、SOには、ユーザーがマップから降りる方法を理解していないユーザーからの質問がたくさんあります。彼らはかなり良い警告を受けましたが。今、それは彼らの問題であり、C#チームの問題ではありません。彼らはドラゴンをマークした。
ハンスパッサント2013

1
@Sayse同期関数と非同期関数の違いを取り除いても、同期的に実装された関数の呼び出しは(WriteLineの例のように)同期的です
talkol

2
「戻り値をTask <>でラップする」メソッドがasync(何らかの契約を満たすため)でなければならない場合を除き、これはおそらく悪い考えです。非同期の欠点(メソッド呼び出しのコストの増加await、呼び出しコードでの使用の必要性)は得られますが、利点は得られません。
スビック2013

1
これは非常に興味深い質問ですが、SOにはあまり適していません。プログラマーへの移行を検討するかもしれません。
Eric Lippert 2013

1
何か足りないかもしれませんが、まったく同じ質問があります。async / awaitが常にペアになっていて、コードが実行の完了を待機する必要がある場合、既存の.NETフレームワークを更新するだけでなく、非同期にする必要があるメソッドを作成します。追加のキーワードを構成せずにデフォルトで非同期にしますか?言語は、それが脱出するように設計されたものにすでになっています-キーワードスパゲッティ。「var」が提案された後、彼らはそれをやめるべきだったと思います。これで、「動的」、asyn / await ...などができました。なぜ.NET-ify javascriptだけなのですか。;))
monstro 2014

回答:


127

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

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

まあ、あなたは誇張しています。すべてのコードが非同期になっていません。2つの「プレーン」整数を一緒に追加すると、結果を待つことはありません。次の2つの追加すると、将来の整数を第三取得するために一緒に将来の整数をそれが何だから-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農産物の副作用、あなたはRの副作用がSの副作用の前に起こることを知っています。しかしasync void M() { await Q(); R(); }、突然それがなくなった場合、がR()発生する前か後に発生するかどうかは保証されませんS()(もちろんM()、待機する場合を除きますが、もちろん、Task待機する必要はありませんN())。

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

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

重要Task<T>なのは、これだけの作業を保証するのに実際に特別なのかどうかは、私には明らかではありません。

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


待機せずに非同期関数が呼び出されても、(一般的なケースでは)意味がありません。この機能を削除する場合、コンパイラーは、関数が非同期かどうかを(それがawaitを呼び出すのか)自分で決定することができます。次に、両方のケース(非同期と同期)で同じ構文を使用し、呼び出しでawaitのみを差別化要因として使用できます。ゾンビの蔓延は解決されました:)
talkol 2013

私はあなたのリクエストに応じてプログラマーでの議論を続けてきました:programmers.stackexchange.com/questions/209872/…–
talkol

@EricLippert-いつものように非常に良い答え:)「高レイテンシ」を明確にできるかどうか知りたいですか?ここにミリ秒単位の一般的な範囲はありますか?私はそれを乱用したくないので、下限境界線が非同期を使用するための場所を見つけようとしています。
トラビスJ

7
@TravisJ:ガイダンスは次のとおりです。UIスレッドを30ミリ秒以上ブロックしないでください。それ以上の場合、ユーザーが一時停止に気付くリスクがあります。
Eric Lippert 2013年

1
何かが同期的または非同期的に行われるかどうかは、変更可能な実装の詳細であることが私に課題です。しかし、その実装の変更は、それを呼び出すコード、それを呼び出すコードなどを通じて波及効果を強制します。それが依存するコードのために、コードを変更することになります。これは、通常、回避するために非常に長い時間を費やします。またはasync/await、抽象化のレイヤーの下に隠されているものが非同期である可能性があるために使用します。
Scott Hannen

23

なぜすべての関数が非同期ではないのですか?

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

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

もう1つは、複雑さと意図の問題です。非同期操作は複雑さを追加します-多くの場合、言語機能はこれを隠しますが、メソッドを作成するasyncことでその使用法が確実に複雑になる場合が多くあります。これは、同期コンテキストがない場合に特に当てはまります。非同期メソッドを使用すると、予期しないスレッドの問題が簡単に発生する可能性があるためです。

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

これはまた、現在のパラダイムを完全に壊し、プロパティの問題を引き起こし(これは事実上、メソッドのペアにすぎません。それらも非同期になりますか?)、フレームワークと言語の設計全体に他の影響があります。

IOにバインドされた多くの作業を行っている場合、asyncパーベイシブを使用することは大きな追加であり、多くのルーチンはそうなることに気付くでしょうasync。ただし、CPUにバインドされた作業を開始するとき、一般に、物事を作ることasyncは実際に良いことではありません- 非同期であるように見えますが、必ずしも真に非同期である必要はないAPIの下でCPUサイクルを使用しているという事実を隠しています。


正確に私が書こうとしているもの(パフォーマンス)、下位互換性は別のものかもしれません、非同期/
待機

同期関数と非同期関数の違いを単純に取り除いても、sqrtを非同期にすることはばかげていません
talkol

@talkol私はこれを好転させると思います-なぜすべての関数呼び出しが非同期の複雑さを引き受けなければならないのですか?
リードコプシー2013

2
@talkol必ずしもそうではない、と私は主張します-非同期は、ブロック自体よりも悪いバグ自体を追加する可能性があります...
Reed Copsey

1
@talkolどのようにawait FooAsync()簡単ですFoo()か?そして、時には小さなドミノ効果の代わりに、常に大きなドミノ効果があり、それを改善と呼びますか?
スビック2013

4

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

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


2
+1-ランダ​​ムな一連の完了と並行して5つの非同期操作を実行するコードのメンタルマップを作成する単純な試みは、ほとんどの人にとって1日十分な苦痛を与えます。非同期(したがって本質的に並列)コードの動作に関する推論は、古き良き同期コードよりもはるかに困難です...
Alexei Levenkov '28

2

拡張性が必要ない場合は、すべてのメソッドを非同期にするのに十分な理由があると思います。選択的にメソッドを非同期にすることは、コードが決して進化せず、メソッドA()が常にCPUにバインドされ(同期を維持)、メソッドB()が常にI / Oにバインドされている(非同期にマークを付ける)場合にのみ機能します。

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

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


これは極端なコメントのようですが、そこには多くの真実があります。第1に、この問題により、「これによりAPIおよびスタックのすべての呼び出し元が壊れる」async / awaitにより、アプリケーションがより強固に結合されます。たとえば、サブクラスがasync / awaitを使用したい場合、Liskov置換の原則を簡単に破ることができます。また、ほとんどのメソッドが非同期/待機を必要としないマイクロサービスアーキテクチャを想像することは困難です。
ItAllABadJoke
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.