Parallel.ForEachとTask.Factory.StartNew


267

以下のコードスニペットの違いは何ですか?どちらもスレッドプールスレッドを使用しませんか?

たとえば、コレクション内の各アイテムの関数を呼び出したい場合、

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

回答:


302

最初ははるかに良いオプションです。

Parallel.ForEachは、内部で、Partitioner<T>コレクションを作業項目に配布するためにを使用します。アイテムごとに1つのタスクを実行するのではなく、これをバッチ処理して、関連するオーバーヘッドを削減します。

2番目のオプションはTask、コレクション内のアイテムごとに1 つをスケジュールします。結果は(ほぼ)同じですが、特に大規模なコレクションの場合、必要以上のオーバーヘッドが発生し、全体的なランタイムが遅くなります。

FYI-使用するパーティショナーは、必要に応じてParallel.ForEachへの適切なオーバーロードを使用して制御できます。詳細については、MSDNのカスタムパーティショナーを参照してください。

実行時の主な違いは、2番目が非同期で動作することです。これは、Parallel.ForEachを使用して複製できます。

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

これにより、パーティショナーを利用できますが、操作が完了するまでブロックしないでください。


8
Parallel.ForEachによって行われるデフォルトのパーティション分割であるIIRCでは、使用可能なハードウェアスレッドの数も考慮されるため、開始するタスクの最適な数を計算する必要がなくなります。マイクロソフトの並列プログラミングパターンの記事をご覧ください。その中にあるすべてのものについての素晴らしい説明があります。
Mal Ross

2
@Mal:ソート...それは実際にはパーティショナーではなく、むしろTaskSchedulerの仕事です。TaskSchedulerはデフォルトで、これを非常にうまく処理する新しいThreadPoolを使用します。
リードコプシー、2011

ありがとう。「私は専門家ではありませんが...」の警告に残すべきであることがわかっていました。:)
マルロス

@ReedCopsey:Parallel.ForEachを介して開始されたタスクをラッパータスクにアタッチする方法 したがって、ラッパータスクで.Wait()を呼び出すと、並行して実行されているタスクが完了するまでハングしますか?
Konstantin Tarkus、2012年

1
@Tarkus複数のリクエストを行う場合は、(Parallelループ内の)各作業項目でHttpClient.GetStringを使用するほうがよいでしょう。非同期オプションを既に並行ループ内に配置する理由はありません。通常は...
Reed Copsey

89

「Parallel.For」と「Task」オブジェクトを使用してメソッドを「1,000,000,000(10億)」回実行する小さな実験を行いました。

プロセッサ時間を測定したところ、Parallelの方が効率的でした。Parallel.Forは、タスクを小さな作業項目に分割し、最適な方法ですべてのコアで並列に実行します。多くのタスクオブジェクトを作成している間(FYI TPLはスレッドプーリングを内部で使用します)、各タスクのすべての実行を移動し、以下の実験から明らかなように、ボックス内により多くのストレスを作成します。

また、基本的なTPLを説明する小さなビデオを作成し、Parallel.Forがコアをhttp://www.youtube.com/watch?v=No7QqSc5cl8を通常のタスクやスレッドと比較してより効率的に利用する方法も示しました。

実験1

Parallel.For(0, 1000000000, x => Method1());

実験2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

プロセッサー時間の比較


これはより効率的であり、スレッドの作成にコストがかかる理由は、実験2は非常に悪い習慣です。
Tim

@ Georgi-それは何が悪いのかについてもっと話すことに気をつけてください。
Shivprasad Koirala 14年

3
すみません、私の間違いです、私は明確にすべきでした。つまり、タスクを1000000000までループで作成するということです。オーバーヘッドは想像を絶するものです。言うまでもなく、Parallelは一度に63を超えるタスクを作成することはできません。これにより、ケースでの最適化がさらに向上します。
Georgi-it

これは1000000000タスクに当てはまります。ただし、画像を繰り返し処理して(繰り返しフラクタルをズーム)、Parallel.Forを実行すると、最後のスレッドが完了するのを待つ間、多くのコアがアイドル状態になります。それをより速くするために、私は自分でデータを64の作業パッケージに細分し、そのためのタスクを作成しました。(その後、Task.WaitAllが完了を待つ)。1〜2のスレッドが(Parallel.For)割り当てられたチャンクを完了するのを待つのではなく、アイドルスレッドが作業パッケージをピックアップして作業を完了するようにするという考え方です。
Tedd Hansen

1
Mehthod1()この例では何をしますか?
Zapnologica

17

Parallel.ForEachは最適化し(新しいスレッドを開始しないこともあります)、ループが終了するまでブロックし、Task.Factoryは各項目の新しいタスクインスタンスを明示的に作成し、終了する前に戻ります(非同期タスク)。Parallel.Foreachの方がはるかに効率的です。


11

私の考えでは、最も現実的なシナリオは、タスクを完了するために重い操作がある場合です。Shivprasadのアプローチは、計算自体よりもオブジェクトの作成/メモリの割り当てに重点を置いています。私は次の方法を呼び出す研究をしました:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

このメソッドの実行には約0.5秒かかります。

Parallelを使用して200回呼び出しました。

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

次に、旧式の方法を使用して200回呼び出しました。

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

最初のケースは26656msで完了し、2番目のケースは24478msで完了しました。何度も繰り返しました。2番目のアプローチの方がわずかに速くなります。


Parallel.Forの使用は昔ながらの方法です。均一でない作業単位には、タスクの使用をお勧めします。Microsoft MVPとTPLの設計者は、タスクを使用するとスレッドをより効率的に使用できる、つまり他のユニットが完了するのを待っている間は多くのスレッドをブロックしないと述べています。
Suncat2000
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.