Tasks APIを使用するためのルール。 パート1

.NETでのタスク処理の登場からほぼ6年が経過しました。 ただし、Taskを使用する場合、依然として混乱が見られます。 ()およびTask.Factoryを実行します 。 プロジェクトコード内のStartNew ()。 これがそれらの類似性に起因すると考えられる場合、C#の動的によりいくつかの問題が発生する可能性があります。



この投稿では、問題、解決策、原因を示してみます。



問題



次のようなコードがあるとします。



static async Task<dynamic> Compute(Task<dynamic> inner) { return await Task.Factory.StartNew(async () => await inner); }
      
      





愛好家への質問:この例に問題はありますか? もしそうなら、どれ? コードはコンパイルされ、所定のタスクの戻り値の型、 awaitを使用する場合の非同期修飾子もコンパイルします。



ConfigureAwaitが欠落していると思いますか? ハハ!



注意 :別の記事については、ConfigureAwaitに関する質問を省略します。



起源



async / awaitイディオムの前は、Tasks APIを使用する主な方法はTask.Factoryメソッドでした。 多数のオーバーロードを持つStartNew ()。 だから、タスク。 Run ()は、スケジューラ(TaskScheduler)などを省略して、このアプローチを少し簡単にします。



 static Task<T> Run<T>(Func<T> inner) { return Task.Run(inner); } static Task<T> RunFactory<T>(Func<T> inner) { return Task.Factory.StartNew(inner); }
      
      





上記の例には特に何もありませんが、ここから違いが始まり、主な問題が発生します-多くの人がTask.Run()は軽量のTask.Factory.StartNew()であると考え始めます。



しかし、これはそうではありません!



より明確にするために、例を考えてみましょう。



 static Task<T> Compute<T>(Task<T> inner) { return Task.Run(async () => await inner); } static async Task<T> ComputeWithFactory<T>(Task<T> inner) { return await await Task.Factory.StartNew(async () => await inner); }
      
      





なに? 二人待ってる? そうです。



それはすべてオーバーロードについてです:



 public static Task<TResult> Run<TResult>(Func<Task<TResult>> function) { // code } public Task<TResult> StartNew<TResult>(Func<TResult> function) { // code }
      
      





両方のメソッドの戻り値の型がTask <TResult>であるという事実にもかかわらず、 Runの入力パラメーターはFunc <Task <TResult >>です。



async () => await inner



の場合、Task.Runは既製のステートマシンを受け取り(awaitはコードをステートマシンに変換する以外の何物でもないことを知っています)、すべてがタスクに変わります。

StartNewは同じものを取得しますが、TResultは既にTask <Task <T >>になります。



-OK、しかし最初の例がコンパイルエラーでクラッシュしない理由 待っていない?

答えは動的です。



ある記事では動的な仕組みについて説明しました。C#のすべてのステートメントは、コンパイルではなくランタイムを参照する呼び出しサイトになります。 同時に、コンパイラー自体がより多くのメタデータをランタイムに渡そうとします。



Compute ()メソッドはTask <dynamic>を使用して返し、コンパイラーにこれらの呼び出しノードを作成させます。

さらに、これは正しいコードです-実行時の結果はTask <Task <dynamic >>になります。



解決策



それは非常に簡単です: Unwrap ()メソッドを使用する必要があります。



動的でないコードでは、2つの待機の代わりに1つを実行できます。



 static async Task<T> ComputeWithFactory<T>(Task<T> inner) { return await Task.Factory.StartNew(async () => await inner).Unwrap(); }
      
      





に適用します



 static async Task<dynamic> Compute(Task<dynamic> inner) { return await Task.Factory.StartNew(async () => await inner).Unwrap(); }
      
      





予想どおり、結果はTask <dynamic>になります。ここでdynamicは、inner'aの正確戻り値ですが、まだ別のタスクではありません。



結論



Task.Factory.StartNew()には常にUnwrap拡張メソッドを使用します。 これにより、コードがより慣用的になり(呼び出しごとに1つ待つ)、動的なトリックが防止されます。



Task.Run()-通常のコンピューティング用。

Task.Factory.StartNew()+ Unwrap()-TaskSchedulerなどを示す通常の計算用




All Articles