C#7.0以降、非同期メソッドはValueTask <T>を返すことができます。説明には、キャッシュされた結果がある場合、または同期コードを介して非同期をシミュレートする場合に使用する必要があると記載されています。しかし、私はまだValueTaskを常に使用することの何が問題なのか、または実際にasync / awaitが最初から値型で構築されなかった理由を理解していません。ValueTaskはいつ仕事に失敗しますか?
C#7.0以降、非同期メソッドはValueTask <T>を返すことができます。説明には、キャッシュされた結果がある場合、または同期コードを介して非同期をシミュレートする場合に使用する必要があると記載されています。しかし、私はまだValueTaskを常に使用することの何が問題なのか、または実際にasync / awaitが最初から値型で構築されなかった理由を理解していません。ValueTaskはいつ仕事に失敗しますか?
回答:
APIドキュメントから(強調を追加):
メソッドは、操作の結果が同期的に利用できる可能性が高い場合や、メソッドが頻繁に呼び出さ
Task<TResult>
れて各呼び出しに新しいを割り当てるコストが非常に高くなる場合に、この値タイプのインスタンスを返すことがあります。の
ValueTask<TResult>
代わりにを使用することにはトレードオフがありますTask<TResult>
。たとえば、aValueTask<TResult>
は、成功した結果が同期的に利用可能な場合に割り当てを回避するのに役立ちますがTask<TResult>
、参照型としてのaは単一のフィールドですが、2つのフィールドも含みます。つまり、メソッド呼び出しは、1つではなく2つのフィールドのデータを返すことになります。これは、コピーするデータが多いことを意味します。また、これらのいずれかを返すメソッドがメソッド内で待機する場合、単一の参照ではなく2つのフィールドである構造体を格納する必要があるasync
ため、そのasync
メソッドのステートマシンが大きくなります。さらに、経由非同期操作の結果を消費する以外の用途のために
await
、ValueTask<TResult>
中に実際に回すより配分をもたらすことができるより多くの畳み込みプログラミングモデル、につながる可能性があります。たとえばTask<TResult>
、キャッシュされたタスクを持つaを共通の結果として返す可能性があるメソッド、またはを検討しValueTask<TResult>
ます。結果の消費者としてそれを使用したい場合Task<TResult>
などの方法ででそのような使用にとして、Task.WhenAll
そしてTask.WhenAny
、ValueTask<TResult>
最初に変換する必要がありますTask<TResult>
使用してAsTask
、キャッシュされたがあれば回避できたはず配分にどのリードTask<TResult>
に使用されていましたそもそも。したがって、非同期メソッドのデフォルトの選択は、
Task
またはを返すことTask<TResult>
です。のValueTask<TResult>
代わりにを使用する必要があるのは、パフォーマンス分析で価値があることが判明した場合のみですTask<TResult>
。
Task
割り当て(最近は小さくて安い)を節約しますが、呼び出し元の既存の割り当てを大きくし、戻り値のサイズを2倍にする(レジスター割り当てに影響を与える)代償を払って。バッファリングされた読み取りのシナリオでは明確な選択ですが、デフォルトですべてのインターフェースに適用することはお勧めしません。
Task
またはValueTask
同期戻り型として使用できます(を使用Task.FromResult
)。しかし、まだの値(あわや)がありますValueTask
あなたが何か持っている場合は期待同期することを。ReadByteAsync
古典的な例です。私は ValueTask
主に新しい「チャネル」(低レベルのバイトストリーム)用に作成されたと考えられ、パフォーマンスが本当に重要なASP.NETコアでも使用される可能性があります。
Task<T>
デフォルトとして使用することをお勧めします。これは、ほとんどの開発者が制限ValueTask<T>
(特に、「1回のみ使用する」ルールと「ブロックしない」ルール)に慣れていないためです。とはいえ、チームのすべての開発者がに満足している場合ValueTask<T>
は、チームレベルの推奨ガイドラインをお勧めしValueTask<T>
ます。
しかし、私はまだValueTaskを常に使用することの問題が何であるか理解していません
構造体タイプは無料ではありません。参照のサイズよりも大きい構造体をコピーすると、参照をコピーするよりも遅くなることがあります。参照よりも大きい構造体を格納すると、参照を格納するよりも多くのメモリが必要になります。参照を登録できる場合、64ビットより大きい構造体は登録されない可能性があります。収集圧力を下げることの利点は、コストを超えることはできません。
パフォーマンスの問題は、エンジニアリング分野で対処する必要があります。目標を立て、目標に対する進捗状況を測定し、目標が達成されない場合にプログラムを変更する方法を決定し、変更が実際に改善されていることを確認する方法を測定します。
async / awaitが最初から値型を使用してビルドされなかった理由
await
Task<T>
型がすでに存在してからずっと後にC#に追加されました。新しいタイプがすでに存在している場合に、新しいタイプを発明することは多少不可解でした。そしてawait
、2012年に出荷されたものに落ち着く前に、非常に多くの設計反復を経験しました。完璧は善の敵です。既存のインフラストラクチャでうまく機能するソリューションを出荷し、ユーザーの要求があれば、後で改善を提供することをお勧めします。
また、ユーザー指定の型をコンパイラーが生成したメソッドの出力にできるようにする新機能により、かなりのリスクとテストの負担が増えることにも注意してください。返すことができる唯一のものが無効またはタスクである場合、テストチームは完全にクレイジーな型が返されるシナリオを考慮する必要はありません。コンパイラ手段のテストプログラムの人が書き込み可能性があるだけでなく、何を考え出すが、何のプログラムがある可能性私たちは、コンパイラはすべての法的プログラムだけでなく、すべての賢明なプログラムをコンパイルしたいので、書き込みに。それは高価です。
誰かがValueTaskが仕事に失敗するときを説明できますか?
モノの目的は、パフォーマンスの向上です。パフォーマンスが測定可能かつ大幅に改善されない場合は、機能しません。それができるという保証はありません。
ValueTask<T>
のサブセットではなく、Task<T>
スーパーセットです。
ValueTask<T>
Tとaの識別された和集合ですTask<T>
ReadAsync<T>
使用可能なT値を同期的に返すために割り当て不要にします(インスタンスTask.FromResult<T>
を割り当てる必要があるを使用するのとは対照的ですTask<T>
)。ValueTask<T>
は待機可能であるため、インスタンスのほとんどの消費はとの区別がつきませんTask<T>
。ValueTaskは構造体であるため、APIの一貫性を損なうことなく、同期的に実行するときにメモリを割り当てない非同期メソッドを作成できます。タスクを返すメソッドとのインターフェースがあると想像してください。このインターフェイスを実装する各クラスは、たまたま同期して実行された場合でも(できればTask.FromResultを使用して)Taskを返す必要があります。もちろん、インターフェイスには同期と非同期の2つの異なるメソッドを使用できますが、「sync over async」と「async over sync」を回避するには、2つの異なる実装が必要です。
したがって、非同期または同期のメソッドを1つ作成することができます。それ以外の点では同じメソッドを1つずつ作成する必要はありません。どこでも使用できますTask<T>
が、多くの場合、何も追加されません。
まあ、それは1つのことを追加します。それは、メソッドが実際に追加の機能を使用するという暗黙の約束を呼び出し元に追加します。 ValueTask<T>
提供ます。私は個人的に、呼び出し元にできるだけ多く伝えるパラメータと戻り値のタイプを選択することを好みます。IList<T>
列挙がカウントを提供できない場合は返さないでください。IEnumerable<T>
できる場合は戻らないでください。コンシューマは、ドキュメントを調べて、同期的に呼び出すことができるメソッドとできないメソッドを知る必要はありません。
将来の設計変更は、そこに説得力のある議論だとは思わない。まったく逆:メソッドがそのセマンティクスを変更する場合、するすべての呼び出しが更新されるまで、ビルドは中断されます。それが望ましくないと考えられる場合(そして私を信じて、私はビルドを壊したくないという欲望に同情しています)、インターフェースのバージョン管理を検討してください。
これが本質的に強いタイピングの目的です。
ショップで非同期メソッドを設計している一部のプログラマーが十分な情報に基づいて決定を下せない場合は、経験の浅いプログラマーそれぞれに上級メンターを割り当て、週に1度コードレビューを行うと役立つ場合があります。推測が間違っている場合は、それを別の方法で行う必要がある理由を説明してください。これは上級者にとってはオーバーヘッドですが、ジュニアをディープエンドで投げて従うべき任意のルールを与えるよりもはるかに速くスピードを上げることができます。
メソッドを作成した人が同期的に呼び出すことができるかどうかわからない場合、地球上で誰が行うのですか?
非同期メソッドを作成する経験の浅いプログラマーがたくさんいる場合、同じ人がそれらを呼び出しているでしょうか?彼らは自分が非同期で安全に呼び出すことができるものを見つけ出す資格がありますか、それとも、これらのものを呼び出す方法に同様に任意のルールを適用し始めますか?
ここでの問題は、戻り値の型ではなく、プログラマが準備ができていない役割に置かれていることです。それは理由があったに違いないので、修正するのは簡単なことではないと私は確信しています。それを説明することは確かに解決策ではありません。しかし、コンパイラを越えて問題をこっそりと探す方法を探すことも、解決策ではありません。
ValueTask<T>
、メソッドが実際にValueTask<T>
追加する機能を使用しているため、メソッドを作成した人がそれを行ったと思います。すべてのメソッドで同じ戻り値の型を使用することが望ましいと思う理由がわかりません。そこでの目標は何ですか?
object
いつかはのint
代わりに返すようにしたいと思うかもしれませんstring
。
.Net Core 2.1にはいくつかの変更があります。.netコア2.1以降、ValueTaskは同期完了アクションだけでなく、非同期完了も表すことができます。また、非ジェネリックValueTask
タイプも承っております。
あなたの質問に関連するStephen Toub コメントを残します:
ガイダンスを形式化する必要がありますが、パブリックAPIの表面積については次のようになると思います。
タスクが最も使いやすさを提供します。
ValueTaskは、パフォーマンスを最適化するためのほとんどのオプションを提供します。
他のユーザーがオーバーライドするインターフェース/仮想メソッドを作成している場合、ValueTaskが正しいデフォルトの選択です。
割り当てが重要となるホットパスでAPIが使用されることが予想される場合は、ValueTaskが適しています。
それ以外の場合、パフォーマンスが重要ではない場合は、より良い保証と使いやすさが提供されるため、デフォルトでTaskになります。
実装の観点からは、返されたValueTaskインスタンスの多くは引き続きTaskによってサポートされます。
機能は.netコア2.1だけでなく使用できます。System.Threading.Tasks.Extensionsパッケージで使用できます。
ValueTask<T>
(割り当ての観点から)実際には非同期の操作を実現しない(その場合、ValueTask<T>
ヒープ割り当てが必要になるため)ことの利点に関係しているのではないかと思います。Task<T>
ライブラリ内で他の多くのサポートを持つという問題もあります。