240

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

たとえば、コレクション内の各項目に対して関数を呼び出したい場合は、

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

vs

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

4 답변


276

最初の方がはるかに良い選択肢です。

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

2番目のオプションは、単一のTaskあなたのコレクションのアイテムごとに。結果は(ほぼ)同じになりますが、これは、特に大規模なコレクションの場合、必要以上にはるかに多くのオーバーヘッドをもたらし、全体的なランタイムを遅くします。

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

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

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

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


  • Parallel.ForEachによって行われるデフォルトのパーティショニングであるIIRCでは、使用可能なハードウェアスレッドの数も考慮されるため、開始するタスクの最適数を考え出す必要がありません。マイクロソフトをチェックしてください。並列プログラミングのパターン記事その中のすべてのものについて素晴らしい説明があります。 - Mal Ross
  • @ Mal:並べ替え...実際にはPartitionerではなく、TaskSchedulerの仕事です。 TaskSchedulerは、デフォルトで、新しいThreadPoolを使用します。これは現在、これを非常にうまく処理します。 - Reed Copsey
  • ありがとう。 「エキスパートはいませんが、...」には参加する必要があります。警告。 :) - Mal Ross
  • @ReedCopsey:Parallel.ForEachを介して開始されたタスクをラッパータスクにアタッチする方法ラッパータスクで.Wait()を呼び出すと、並行して実行中のタスクが完了するまでハングしますか。 - Konstantin Tarkus
  • @Tarkus複数のリクエストを作成する場合は、各作業項目でHttpClient.GetStringを使用することをお勧めします(パラレルループ内)。すでに並行しているループ内に非同期オプションを設定する理由はありません。通常は... - Reed Copsey

16

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


74

私は "Parallel.For"と "Task"オブジェクトを使って "1000000000"メソッドを実行する小さな実験をしました。

プロセッサ時間を測定したところ、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();
}

Processor time comparison


  • これはより効率的であり、スレッドを作成するのはコストがかかるという理由の背後にある実験2は非常に悪い習慣です。 - Tim
  • @ Georgi-それは悪いことについてもっと話すことを気にしてください。 - Shivprasad Koirala
  • すみません、私の過ち、私ははっきりさせるべきでした。タスクを1000000000までループで作成することを意味します。オーバーヘッドは想像できません。言うまでもなく、Parallelは一度に63個を超えるタスクを作成することはできません。そのため、このケースでは、タスクはさらに最適化されます。 - Georgi-it
  • これは1000000000タスクに当てはまります。しかし、私がイメージを処理するとき(繰り返し、ズームフラクタル)、Parallel.Forを実行すると、最後のスレッドが終了するのを待つ間、多くのコアがアイドル状態になります。それをより速くするために、私は自分自身でデータを64の作業パッケージに細分化し、それに対するタスクを作成しました。 1〜2スレッドが(Parallel.For)割り当てられたチャンクを完了するのを待つのではなく、アイドルスレッドに作業パッケージを拾い上げて作業を完了させるのが目的です。 - Tedd Hansen
  • なにがMehthod1()この例ではどうですか? - Zapnologica

7

私の考えでは、最も現実的なシナリオは、タスクが完了するのに重い操作を持つときです。 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番目のアプローチが限界より速いです。

リンクされた質問


最近の質問