.NETでTask.WaitAllとTask.WhenAllを使用する場合

TPL(Task Parallel Library)は、最近のバージョンの.NETFrameworkで追加された最も興味深い新機能の1つです。Task.WaitAllメソッドとTask.WhenAllメソッドは、TPLで2つの重要で頻繁に使用されるメソッドです。

Task.WaitAllは、他のすべてのタスクの実行が完了するまで、現在のスレッドをブロックします。Task.WhenAllメソッドは、他のすべてのタスクが完了した場合にのみ完了するタスクを作成するために使用されます。

したがって、Task.WhenAllを使用している場合は、完了していないタスクオブジェクトを取得します。ただし、ブロックはしませんが、プログラムは実行できます。それどころか、Task.WaitAllメソッド呼び出しは実際にはブロックし、他のすべてのタスクが完了するのを待ちます。

基本的に、Task.WhenAllは完了していないタスクを提供しますが、指定されたタスクが実行を完了するとすぐにContinueWithを使用できます。Task.WhenAllもTask.WaitAllも実際にはタスクを実行しないことに注意してください。つまり、これらのメソッドによってタスクが開始されることはありません。これが、ContinueWithがTask.WhenAllでどのように使用されるかです。 

Task.WhenAll(taskList).ContinueWith(t => {

  //ここにコードを記述します

});

Microsoftのドキュメントに記載されているように、Task.WhenAllは、「列挙可能なコレクション内のすべてのTaskオブジェクトが完了したときに完了するタスクを作成します」。

Task.WhenAllとTask.WaitAll

これら2つの方法の違いを簡単な例で説明しましょう。 UIスレッドで何らかのアクティビティを実行するタスクがあるとします。たとえば、いくつかのアニメーションをユーザーインターフェイスに表示する必要があります。これで、Task.WaitAllを使用すると、ユーザーインターフェイスがブロックされ、関連するすべてのタスクが完了してブロックが解放されるまで更新されません。ただし、同じアプリケーションでTask.WhenAllを使用している場合、UIスレッドはブロックされず、通常どおり更新されます。

では、これらの方法のどれをいつ使用する必要がありますか?インテントが同期的にブロックしているときにWaitAllを使用して、結果を取得できます。ただし、非同期を利用する場合は、WhenAllバリアントを使用する必要があります。現在のスレッドをブロックすることなく、Task.WhenAllを待つことができます。したがって、非同期メソッド内でTask.WhenAllでawaitを使用することをお勧めします。

Task.WaitAllは、保留中のすべてのタスクが完了するまで現在のスレッドをブロックしますが、Task.WhenAllはタスクオブジェクトを返します。1つ以上のタスクが例外をスローすると、Task.WaitAllはAggregateExceptionをスローします。1つ以上のタスクが例外をスローし、Task.WhenAllメソッドを待つと、AggregateExceptionがアンラップされ、最初のタスクだけが返されます。

Task.Runをループで使用しないでください

並行アクティビティを実行する場合は、タスクを使用できます。高度な並列処理が必要な場合、タスクは決して良い選択ではありません。ASP.Netでスレッドプールスレッドを使用しないことを常にお勧めします。したがって、ASP.NetでTask.RunまたはTask.factory.StartNewを使用することは控えてください。

Task.Runは、常にCPUバウンドコードに使用する必要があります。Task.Runは、ASP.Netアプリケーション、またはASP.Netランタイムを利用するアプリケーションでは適切な選択ではありません。これは、作業をThreadPoolスレッドにオフロードするだけだからです。ASP.Net Web APIを使用している場合、要求はすでにThreadPoolスレッドを使用しています。したがって、ASP.Net Web APIアプリケーションでTask.Runを使用する場合は、理由がない限り、作業を別のワーカースレッドにオフロードすることでスケーラビリティを制限しているだけです。

Task.Runをループで使用することには欠点があることに注意してください。ループ内でTask.Runメソッドを使用すると、複数のタスクが作成されます(作業単位または反復ごとに1つずつ)。ただし、ループ内でTask.Runを使用する代わりにParallel.ForEachを使用すると、アクティビティを実行するために必要以上のタスクが作成されないように、Partitionerが作成されます。これにより、コンテキストスイッチが多すぎないようにし、システム内の複数のコアを活用できるため、パフォーマンスが大幅に向上する可能性があります。

Parallel.ForEachは、コレクションを作業項目に分散するために、内部でPartitionerを使用することに注意してください。ちなみに、この配布はアイテムのリスト内のタスクごとに発生するのではなく、バッチとして発生します。これにより、関連するオーバーヘッドが削減され、パフォーマンスが向上します。つまり、ループ内でTask.RunまたはTask.Factory.StartNewを使用すると、ループ内の反復ごとに新しいタスクが明示的に作成されます。Parallel.ForEachは、システム内の複数のコアに作業負荷を分散することで実行を最適化するため、はるかに効率的です。