問題
いくつかのコードがあります。
static Task<TResult> ComputeAsync<TResult>(Func<TResult> highCpuFunc) { var tcs = new TaskCompletionSource<TResult>(); try { TResult result = highCpuFunc(); tcs.SetResult(result); // some evil code } catch (Exception exc) { tcs.SetException(exc); } return tcs.Task; }
そして使用例:
try { Task.WaitAll(ComputeAsync(() => { // do work })); } catch (AggregateException) { } Console.WriteLine("Everything is gonna be ok");
上記のコードと例に問題はありますか? もしそうなら、どれ? AggregateExceptionをキャッチしているようです。 すべてが大丈夫ですか?
NB :タスクのキャンセルのトピックは次回に明らかにされるので、キャンセルトークンがないことは考慮しません。
起源
タスクの概念は非同期の概念と非常に密接に関連しており、非同期性はマルチスレッド実行と混同される場合があります。 そしてこれは、順番に、すべての挑戦がどこかで実行されるタスクであるという結論につながります。
タスクは、呼び出し元のコードと同じスレッドで実行できます。 さらに、タスクの実行は必ずしも命令の実行を意味するわけではありません-たとえば、単にTask.FromResultである場合があります。
そのため、問題No. 1はユースケースにあります。InvalidOperationException(それが少し低くなるのは明らかです)またはAggregateExceptionとともに他の例外をキャッチする必要があります。
Task.WhenAllおよびco。 メソッドは
throws AggregateException, ArgumentNullException, ObjectDisposedException
を
throws AggregateException, ArgumentNullException, ObjectDisposedException
するように文書化されています-これはtrueです。
ただし、コード実行のシーケンスを理解する必要があります。ComputeAsyncの本体が呼び出しスレッドで実行され始めた場合、Task.WhenAllに到達しません。 これは少しですが明らかではありませんが。
正しいオプション:
try { Task.WaitAll(ComputeAsync(() => { // do work })); } catch (AggregateException) { } catch (InvalidOperationException) { } Console.WriteLine("Everything is gonna be ok");
OK、整理しました。 どうぞ
TaskCompletionSourceクラスによって提供されるAPI自体は非常に直感的です。 メソッドSetResult 、 SetCanceled 、 SetExceptionは、それ自体を物語っています。 しかし、ここに問題があります。それらはタスク全体の状態を操作します。
うーん...すでにトリックを理解しましたか? さらに詳しく考えてみましょう。
ComputeAsyncメソッドにはSetResultが設定されたコードのセクションがあり、タスクの状態をRanToCompletionに変更します。
その後、(ヒントのように)
evil code
の行で例外がスローされると、例外が処理され、SetExceptionでキャプチャされます。これは、タスクの状態を変更するための2番目の試みです。
この場合、Taskクラス自体の状態は不変です。
NB :なぜこの振る舞いは良いのですか? 例を考えてみましょう:
static async Task<bool> ReadContentTwice() { using (var stringReader = new StringReader("blabla")) { Task<string> task = stringReader.ReadToEndAsync(); string content = await task; // something happens with task. oh no! string contentOnceAgain = await task; return content.Equals(contentOnceAgain); } }
タスクの状態が変更される可能性がある場合、これは非決定的なコード動作の状況につながります。 そして、 可変構造体は「悪」であるというルールを知っています(ただし、タスクはクラスですが、動作の問題は関連しています)。
残りは単純です-InvalidOperationExceptionと何とか何とか。
解決策
すべてが非常に明白です。常にメソッドを終了する直前にSetResultを呼び出します。
順序付けられたSetResult
static Task<TResult> ComputeAsync<TResult>(Func<TResult> highCpuFunc) { var tcs = new TaskCompletionSource<TResult>(); try { TResult result = highCpuFunc(); // some evil code // set result as last action tcs.SetResult(result); } catch (Exception exc) { tcs.SetException(exc); } return tcs.Task; }
- なぜTrySetResult 、 TrySetCanceled 、 TrySetExceptionメソッドを考慮しないのですか?!
これらを使用するには、質問に答える必要があります。
- TaskCompletionSourceの使用範囲自体は、このメソッドのみに制限されていますか?
上記の質問に対する答えがNOの場合、TryXXXを使用してください。 これには、APM、EAPも含まれます。
コードが元の例のように単純な場合-メソッドの単純な順序。
ボーナストラック
毎回、Task.FromResultを呼び出すことは非効率的です。 なぜあなたの記憶を無駄にしますか? これを行うには、フレームワークの組み込み機能を使用できます...
そうです。 CompletedTaskの概念は、.NET 4.6でのみ導入されました 。 そして(あなたが推測したように)いくつかの特異性があります。
フレッシュから始めましょう。新しいプロパティであるTask.CompletedTask:プロパティは、タスクタイプの単なる静的プロパティです( 非ジェネリックオプションが何であるか正確に注意したい)。 いいでしょう めったに牙は結果を出さないので、それが便利になることはまずありません。
また...ドキュメントには、 常に同じインスタンスが返されるとは限りません 。 意図的に作られました。
実際にコード
/// <summary>Gets a task that's already been completed successfully.</summary> /// <remarks>May not always return the same instance.</remarks> public static Task CompletedTask { get { var completedTask = s_completedTask; if (completedTask == null) s_completedTask = completedTask = new Task(false, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default(CancellationToken)); // benign initialization race condition return completedTask; } }
完了したタスクをチェックするときに、Task.CompletedTaskの値(つまり、参照)とキャッシュしたり比較したりしない。
この問題の解決策は非常に簡単です。
public static class CompletedTaskSource<T> { private static readonly Task<T> CompletedTask = Task.FromResult(default(T)); public static Task<T> Task { get { return CompletedTask; } } }
そしてそれだけです。 幸いなことに、.NET 4には、.NET 4 +用のC#5コードをコンパイルできる独立したMicrosoft Async NuGetパッケージがあり、欠落しているTask.FromResultなどをもたらします。