C#でValueTask <T>よりもTask <T>を使用するのはなぜですか?


168

C#7.0以降、非同期メソッドはValueTask <T>を返すことができます。説明には、キャッシュされた結果がある場合、または同期コードを介して非同期をシミュレートする場合に使用する必要があると記載されています。しかし、私はまだValueTaskを常に使用することの何が問題なのか、または実際にasync / awaitが最初から値型で構築されなかった理由を理解していません。ValueTaskはいつ仕事に失敗しますか?


7
私はそれがValueTask<T>(割り当ての観点から)実際には非同期の操作を実現しない(その場合、ValueTask<T>ヒープ割り当てが必要になるため)ことの利点に関係しているのではないかと思います。Task<T>ライブラリ内で他の多くのサポートを持つという問題もあります。
Jon Skeet 2017年

3
@JonSkeetの既存のライブラリーは問題ですが、これはタスクが最初からValueTaskである必要があるのか​​という疑問を投げかけますか?実際の非同期のものにそれを使用するとき、利点は存在しないかもしれませんが、それは有害ですか?
スティルガー2017年

8
私が伝えられないほどの知恵については、github.com / dotnet / corefx / issues / 4708#issuecomment-160658188を参照してください:)
Jon Skeet


3
@JoelMuellerプロットは厚くなります:)
スティルガー

回答:


245

APIドキュメントから(強調を追加):

メソッドは、操作の結果が同期的に利用できる可能性が高い場合、メソッドが頻繁に呼び出さTask<TResult>れて各呼び出しに新しいを割り当てるコストが非常に高くなる場合に、この値タイプのインスタンスを返すことがあります。

ValueTask<TResult>代わりにを使用することにはトレードオフがありますTask<TResult>。たとえば、a ValueTask<TResult>は、成功した結果が同期的に利用可能な場合に割り当てを回避するのに役立ちますがTask<TResult>、参照型としてのaは単一のフィールドですが、2つのフィールドも含みます。つまり、メソッド呼び出しは、1つではなく2つのフィールドのデータを返すことになります。これは、コピーするデータが多いことを意味します。また、これらのいずれかを返すメソッドがメソッド内で待機する場合、単一の参照ではなく2つのフィールドである構造体を格納する必要があるasyncため、そのasyncメソッドのステートマシンが大きくなります。

さらに、経由非同期操作の結果を消費する以外の用途のためにawaitValueTask<TResult>中に実際に回すより配分をもたらすことができるより多くの畳み込みプログラミングモデル、につながる可能性があります。たとえばTask<TResult>、キャッシュされたタスクを持つaを共通の結果として返す可能性があるメソッド、またはを検討しValueTask<TResult>ます。結果の消費者としてそれを使用したい場合Task<TResult>などの方法ででそのような使用にとして、Task.WhenAllそしてTask.WhenAnyValueTask<TResult>最初に変換する必要がありますTask<TResult>使用してAsTask、キャッシュされたがあれば回避できたはず配分にどのリードTask<TResult>に使用されていましたそもそも。

したがって、非同期メソッドのデフォルトの選択は、Taskまたはを返すことTask<TResult>です。のValueTask<TResult>代わりにを使用する必要があるのは、パフォーマンス分析で価値があることが判明した場合のみですTask<TResult>


7
@MattThomas:単一のTask割り当て(最近は小さくて安い)を節約しますが、呼び出し元の既存の割り当てを大きくし、戻り値のサイズを2倍にする(レジスター割り当てに影響を与える)代償を払って。バッファリングされた読み取りのシナリオでは明確な選択ですが、デフォルトですべてのインターフェースに適用することはお勧めしません。
Stephen Cleary 2017年

1
そうです、TaskまたはValueTask同期戻り型として使用できます(を使用Task.FromResult)。しかし、まだの値(あわや)がありますValueTaskあなたが何か持っている場合は期待同期することを。ReadByteAsync古典的な例です。私 ValueTask主に新しい「チャネル」(低レベルのバイトストリーム)用に作成されたと考えられ、パフォーマンスが本当に重要なASP.NETコアでも使用される可能性があります。
Stephen Cleary

1
ああ、その特定のコメントに何か追加することがあるかどうか疑問に思っていました;)
julealgon

2
このPRは ValueTaskを好むの上でバランスを切り替えますか?(参照:blog.marcgravell.com/2019/08/…
stuartd

2
@stuartd:今のところ、私はまだTask<T>デフォルトとして使用することをお勧めします。これは、ほとんどの開発者が制限ValueTask<T>(特に、「1回のみ使用する」ルールと「ブロックしない」ルール)に慣れていないためです。とはいえ、チームのすべての開発者がに満足している場合ValueTask<T>は、チームレベルの推奨ガイドラインをお勧めしValueTask<T>ます。
Stephen Cleary

104

しかし、私はまだValueTaskを常に使用することの問題が何であるか理解していません

構造体タイプは無料ではありません。参照のサイズよりも大きい構造体をコピーすると、参照をコピーするよりも遅くなることがあります。参照よりも大きい構造体を格納すると、参照を格納するよりも多くのメモリが必要になります。参照を登録できる場合、64ビットより大きい構造体は登録されない可能性があります。収集圧力を下げることの利点は、コストを超えることはできません。

パフォーマンスの問題は、エンジニアリング分野で対処する必要があります。目標を立て、目標に対する進捗状況を測定し、目標が達成されない場合にプログラムを変更する方法を決定し、変更が実際に改善されていることを確認する方法を測定します。

async / awaitが最初から値型を使用してビルドされなかった理由

awaitTask<T>型がすでに存在してからずっと後にC#に追加されました。新しいタイプがすでに存在している場合に、新しいタイプを発明することは多少不可解でした。そしてawait、2012年に出荷されたものに落ち着く前に、非常に多くの設計反復を経験しました。完璧は善の敵です。既存のインフラストラクチャでうまく機能するソリューションを出荷し、ユーザーの要求があれば、後で改善を提供することをお勧めします。

また、ユーザー指定の型をコンパイラーが生成したメソッドの出力にできるようにする新機能により、かなりのリスクとテストの負担が増えることにも注意してください。返すことができる唯一のものが無効またはタスクである場合、テストチームは完全にクレイジーな型が返されるシナリオを考慮する必要はありません。コンパイラ手段のテストプログラムの人が書き込み可能性があるだけでなく、何を考え出すが、何のプログラムがある可能性私たちは、コンパイラはすべての法的プログラムだけでなく、すべての賢明なプログラムをコンパイルしたいので、書き込みに。それは高価です。

誰かがValueTaskが仕事に失敗するときを説明できますか?

モノの目的は、パフォーマンスの向上です。パフォーマンスが測定可能かつ大幅に改善されない場合は、機能しません。それができるという保証はありません。


23

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度コードレビューを行うと役立つ場合があります。推測が間違っている場合は、それを別の方法で行う必要がある理由を説明してください。これは上級者にとってはオーバーヘッドですが、ジュニアをディープエンドで投げて従うべき任意のルールを与えるよりもはるかに速くスピードを上げることができます。

メソッドを作成した人が同期的に呼び出すことができるかどうかわからない場合、地球上誰が行うのですか?

非同期メソッドを作成する経験の浅いプログラマーがたくさんいる場合、同じ人がそれらを呼び出しているでしょうか?彼らは自分が非同期で安全に呼び出すことができるものを見つけ出す資格がありますか、それとも、これらのものを呼び出す方法に同様に任意のルールを適用し始めますか?

ここでの問題は、戻り値の型ではなく、プログラマが準備ができていない役割に置かれていることです。それは理由があったに違いないので、修正するのは簡単なことではないと私は確信しています。それを説明することは確かに解決策ではありません。しかし、コンパイラを越えて問題をこっそりと探す方法を探すことも、解決策ではありません。


4
メソッドがを返すのを見た場合ValueTask<T>、メソッドが実際にValueTask<T>追加する機能を使用しているため、メソッドを作成した人がそれを行ったと思います。すべてのメソッドで同じ戻り値の型を使用することが望ましいと思う理由がわかりません。そこでの目標は何ですか?
15ee8f99-57ff-4f92-890c-b56153 2017年

2
目標1は、実装の変更を許可することです。現在結果をキャッシュしていないが、後でキャッシュコードを追加するとどうなりますか?目標2は、ValueTaskが役立つ場合をすべてのメンバーが理解するわけではないチームのための明確なガイドラインを持つことです。使用に支障がない場合は、常に実行してください。
スティルガー2017年

6
私の見解では、これは、すべてのメソッドを返すようなもので、objectいつかはのint代わりに返すようにしたいと思うかもしれませんstring
15ee8f99-57ff-4f92-890c-b56153 2017年

3
たぶん、「なぜオブジェクトではなく文字列を返すのか」という質問に答えると、タイプセーフの欠如を指摘して簡単に答えることができます。ValueTaskをどこでも使用しない理由は、はるかに微妙なようです。
スティルガー2017年

3
@スティルガー私が言うことの1つ:微妙な問題は明白な問題よりもあなたを心配すべきです。
15ee8f99-57ff-4f92-890c-b56153 2017年

20

.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パッケージで使用できます。


1
より多くのスティーブンから今日:blogs.msdn.microsoft.com/dotnet/2018/11/07/...
マイクCheel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.