9

Parallel.For が次のシナリオで多数のスレッドを上回ることができる理由を理解しようとしています: 並列で処理できるジョブのバッチを検討してください。これらのジョブの処理中に、新しい作業が追加される場合があり、それも処理する必要があります。解決策は次のParallel.Forようになります。

var jobs = new List<Job> { firstJob };
int startIdx = 0, endIdx = jobs.Count;
while (startIdx < endIdx) {
  Parallel.For(startIdx, endIdx, i => WorkJob(jobs[i]));
  startIdx = endIdx; endIdx = jobs.Count;
}

これは、Parallel.For を同期する必要がある場合が複数回あることを意味します。パン優先グラフ アルゴリズム アルゴリズムを考えてみましょう。同期の数は非常に多くなります。時間の無駄ですよね?

昔ながらのスレッド化アプローチで同じことを試してみてください:

var queue = new ConcurrentQueue<Job> { firstJob };
var threads = new List<Thread>();
var waitHandle = new AutoResetEvent(false);
int numBusy = 0;
for (int i = 0; i < maxThreads; i++) 
  threads.Add(new Thread(new ThreadStart(delegate {
    while (!queue.IsEmpty || numBusy > 0) {
      if (queue.IsEmpty)
        // numbusy > 0 implies more data may arrive
        waitHandle.WaitOne();

      Job job;
      if (queue.TryDequeue(out job)) {
        Interlocked.Increment(ref numBusy);
        WorkJob(job); // WorkJob does a waitHandle.Set() when more work was found
        Interlocked.Decrement(ref numBusy);
      }
    }
    // others are possibly waiting for us to enable more work which won't happen
    waitHandle.Set(); 
})));
threads.ForEach(t => t.Start());
threads.ForEach(t => t.Join());

もちろん、Parallel.Forコードははるかにきれいですが、私が理解できないことは、さらに高速です! タスクスケジューラはそんなに良いですか?同期は取り除かれ、忙しい待機はありませんが、スレッド化されたアプローチは一貫して遅くなります(私にとって)。どうしたの?スレッド化アプローチを高速化できますか?

編集:すべての回答に感謝します。複数の回答を選択できれば幸いです。私は、実際に可能な改善も示すものを選択しました。

4

3 に答える 3

14

2 つのコード サンプルは実際には同じではありません。

Parallel.ForEach()限られた量のスレッドを使用し、それらを再利用します。2 番目のサンプルは、多数のスレッドを作成する必要があるため、すでに遅れをとっています。それには時間がかかります。

そして、の値はmaxThreads何ですか?Parallel.ForEach()それは動的であるため、非常に重要です。

タスクスケジューラはそんなに良いですか?

それはかなり良いです。また、TPL はワークスティーリングやその他の適応技術を使用しています。あなたはこれ以上うまくやるのに苦労するでしょう。

于 2012-10-25T14:06:30.200 に答える
4

Parallel.For は、実際には項目を単一の作業単位に分割しません。使用する予定のスレッド数と実行する反復回数に基づいて、すべての作業を (早い段階で) 分割します。次に、各スレッドがそのバッチを同期的に処理します (おそらく、ワーク スティーリングを使用するか、いくつかの余分なアイテムを保存して、最後に負荷分散します)。このアプローチを使用することで、ワーカー スレッドは実質的に互いに待機することはありませんが、反復ごとに前後に大量の同期を使用しているため、スレッドは常に互いに待機しています。

その上、スレッド プール スレッドを使用しているため、必要なスレッドの多くが既に作成されている可能性が高く、これは別の利点です。

同期に関しては、Parallel.For の全体的なポイントは、すべての反復を並行して実行できることです。そのため、(少なくともそのコードでは) 実行する必要がある同期はほとんどありません。

もちろん、スレッド数の問題もあります。スレッドプールには、現在のハードウェア、他のアプリケーションからの負荷などに基づいて、その瞬間に必要なスレッド数を判断するのに役立つ非常に優れたアルゴリズムとヒューリスティックが多数あります。使用しているスレッドが多すぎる可能性があります。または十分なスレッドがありません。

また、アイテムの数は開始する前にはわからないため、複数のループParallel.ForEachではなく使用することをお勧めします。Parallel.For現在の状況に合わせて単純に設計されているため、ヒューリスティックがより適切に適用されます。(また、よりクリーンなコードになります。)

BlockingCollection<Job> queue = new BlockingCollection<Job>();

//add jobs to queue, possibly in another thread
//call queue.CompleteAdding() when there are no more jobs to run

Parallel.ForEach(queue.GetConsumingEnumerable(),
    job => job.DoWork());
于 2012-10-25T14:12:48.727 に答える
2

一連の新しいスレッドを作成し、Parallel.For が Threadpool を使用しています。C# スレッドプールを利用していればパフォーマンスは向上しますが、それを行う意味はありません。

独自のソリューションを展開することをためらいます。カスタマイズが必要なまれなケースがある場合は、TPL を使用してカスタマイズします。

于 2012-10-25T14:11:02.917 に答える