web-dev-qa-db-ja.com

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

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

122
Stilgar

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

メソッドは、操作の結果が同期的に利用可能になる可能性が高い場合にこの値型のインスタンスを返すことがありますおよびメソッドが呼び出されると予想される場合に多くの場合、呼び出しごとに新しいTask<TResult>を割り当てるコストは法外に高くなります。

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

さらに、awaitを介して非同期操作の結果を使用する以外の用途では、ValueTask<TResult>がより複雑なプログラミングモデルにつながり、実際にはより多くの割り当てにつながる可能性があります。たとえば、一般的な結果としてキャッシュされたタスクを持つTask<TResult>またはValueTask<TResult>を返す可能性のあるメソッドを考えます。結果のコンシューマーがTask<TResult>Task.WhenAllなどのメソッドで使用するなど、Task.WhenAnyとして使用する場合、最初にValueTask<TResult>AsTaskを使用してTask<TResult>に変換する必要があります。これにより、割り当てが回避されますキャッシュされたTask<TResult>が最初に使用されていた場合。

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

186
Stephen Cleary

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

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

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

async/awaitが最初から値型で構築されなかった理由。

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

また、ユーザー指定の型をコンパイラー生成メソッドの出力として使用できるようにする新しい機能は、かなりのリスクとテストの負担を追加することに注意してください。返すことができるのがvoidまたはタスクのみである場合、テストチームは、絶対に狂ったタイプが返されるシナリオを考慮する必要はありません。コンパイラをテストすることは、人々が書く可能性のあるプログラムだけでなく、書くプログラムが可能であるかを把握することを意味します。賢明なプログラム。それは高価です。

ValueTaskがジョブを実行できない場合を誰かが説明できますか?

事の目的はパフォーマンスの改善です。 測定可能および大幅にパフォーマンスが向上しない場合、ジョブを実行しません。それが保証されるわけではありません。

83
Eric Lippert

ValueTask<T>Task<T>のサブセットではなく、スーパーセットです

ValueTask<T>は、TとTask<T>の識別された共用体であり、ReadAsync<T>が割り当てられたT値を同期的に返すために割り当て不要になります(Task.FromResult<T>を使用すると、Task<T>インスタンスを割り当てる必要があります)。 ValueTask<T>は待機可能であるため、ほとんどのインスタンスの消費はTask<T>と区別できません。

構造体であるValueTaskを使用すると、APIの整合性を損なうことなく、同期実行時にメモリを割り当てない非同期メソッドを作成できます。タスクを返すメソッドを備えたインターフェースを想像してください。このインターフェイスを実装する各クラスは、偶然に同期して実行される場合でもTaskを返す必要があります(できればTask.FromResultを使用)。もちろん、同期メソッドと非同期メソッドの2つの異なるメソッドをインターフェース上に持つことができますが、これには「sync over async」と「async over sync」を避けるために2つの異なる実装が必要です。

したがって、非同期または同期のいずれかであるメソッドを1つ記述することができます。それ以外の場合は、それぞれ同じメソッドを1つ記述することができます。 Task<T>を使用する場所ならどこでも使用できますが、多くの場合、何も追加されません。

まあ、それは一つのことを追加します:メソッドがValueTask<T>が提供する追加機能を実際に使用するという暗黙の約束を呼び出し元に追加します。個人的には、可能な限り発信者に伝えるパラメータと戻り値のタイプを選択することを好みます。列挙がカウントを提供できない場合は、IList<T>を返さないでください。可能であればIEnumerable<T>を返さないでください。消費者は、どのメソッドを同期的に適切に呼び出すことができ、どのメソッドを呼び出せないかを知るためにドキュメントを調べる必要はありません。

将来のデザインの変更は説得力のある議論とは思わない。まったく逆:メソッドのセマンティクスが変更された場合、そのメソッドへのすべての呼び出しがそれに応じて更新されるまで、shouldはビルドを中断します。それが望ましくないと思われる場合(そして、私を信じて、ビルドを壊さないという願望に同情的です)、インターフェースのバージョン管理を検討してください。

これは本質的に強い型付けの目的です。

ショップで非同期メソッドを設計するプログラマーの一部が情報に基づいた決定を下すことができない場合、経験の浅いプログラマーのそれぞれにシニアメンターを割り当て、毎週コードレビューを行うと役立つ場合があります。推測が間違っている場合は、なぜ別の方法で行う必要があるかを説明します。上級者にとってはオーバーヘッドですが、後輩を深く投げて従うべきarbitrary意的なルールを与えるよりもずっと早く後輩をスピードアップさせます。

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

非同期メソッドを書いている経験の浅いプログラマーがたくさんいるなら、同じ人たちも非同期メソッドを呼んでいますか?彼らは非同期を呼び出すのに安全なものを自分で判断する資格がありますか、またはそれらを呼び出す方法に同様の任意のルールを適用し始めますか?

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

21
Ed Plunkett

。Net Core 2.1の変更 があります。 .netコア2.1以降のValueTaskは、完了した同期アクションだけでなく、完了した非同期も表すことができます。さらに、非汎用ValueTask型を受け取ります。

私はスティーブン・トーブを去ります コメント これはあなたの質問に関連しています:

私たちはまだガイダンスを形式化する必要がありますが、パブリックAPIの表面領域については次のようなものになると期待しています。

  • タスクは最も使いやすさを提供します。

  • ValueTaskは、パフォーマンスを最適化するためのほとんどのオプションを提供します。

  • 他のユーザーがオーバーライドするインターフェース/仮想メソッドを作成している場合、ValueTaskがデフォルトの正しい選択です。
  • 割り当てが重要となるホットパスでAPIが使用されることが予想される場合は、ValueTaskが適しています。
  • それ以外の場合、パフォーマンスが重要でない場合は、デフォルトでTaskが使用されます。これにより、保証と使いやすさが向上します。

実装の観点から見ると、返されたValueTaskインスタンスの多くは、タスクによって引き続きサポートされます。

機能は、.netコア2.1だけでなく使用できます。 System.Threading.Tasks.Extensionsパッケージで使用できます。

11
unsafePtr