2012-10-25 5 views
6

私はParallel.Forが次のようなシナリオで多くのスレッドより優れたパフォーマンスを発揮できる理由を理解しようとしています。並列処理できるジョブのバッチを考えてみましょう。これらのジョブを処理している間に、新しい作業が追加される可能性があります。新しい作業が処理される必要があります。以下のようになり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コードは、もちろん非常にきれいですが、私が理解することはできません、それは同様であっても高速です!タスクスケジューラはそれだけでいいですか?同期は絶滅していました。待ち時間がなくなりましたが、スレッドアプローチは一貫して遅いです(私にとって)。どうしたの?スレッディングのアプローチをより速くすることはできますか?

編集:すべての回答ありがとう、私は複数のものを選ぶことができたらいいですね。私は実際の可能性のある改善を示すものと一緒に行くことにしました。

+1

すでにクリーンなソリューションがあれば、なぜそれを速くしたいのですか? – iMortalitySX

+0

明白な欠点が排除できるので、私は思います。 –

+0

閉じる質問[PLinqは本質的にSystem.Threading.Tasks.Parallel.ForEachより高速ですか?](http://stackoverflow.com/questions/5196293/is-plinq-inherently-faster-than-system-threading-tasks-parallel- foreach) – iMortalitySX

答えて

12

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

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

maxThreadsの値は何ですか?非常に重要な、Parallel.ForEach()のそれは動的です。

タスクスケジューラはそれほど優れていますか?

かなり良いです。そして、TPLは、仕事盗みや他の適応技術を使用しています。あなたはもっとうまくやるのに苦労します。

+0

スレッド化された例は、作成したスレッドも再利用します。それはあなたが意味するものならば、それぞれの仕事のためのものではなく、限られた数を始めます。 –

+0

それに私を打つ、スレッドプールの使用ではない。 http://stackoverflow.com/questions/230003/thread-vs-threadpool –

+0

@ジャスティン:ああ、良いリファレンス。ありがとう。 –

1

新しいスレッドを作成し、Parallel.Forがスレッドプールを使用しています。 C#スレッドプールを利用していれば、より良いパフォーマンスが得られますが、実際にそれを行うことに意味はありません。

私はあなた自身の解決策を展開することを恥ずかしく思っています。カスタマイズが必要なコーナーケースがある場合は、TPLを使用してカスタマイズしてください。

3

Parallel.Forは実際にはアイテムを1つの作業単位に分割しません。それは、使用する予定のスレッドの数と実行される反復の数に基づいて、すべての作業を(早期に)分割します。その後、各スレッドはそのバッチを同期的に処理します(おそらく、ワークスチールを使用するか、最後に負荷バランスを取るためにいくつかの余分なアイテムを保存します)。このアプローチを使用することで、ワーカースレッドはお互いに待っていることはほとんどありませんが、毎回の繰り返しの前後で頻繁に使用されるため、スレッドは常に互いを待っています。

スレッドプールスレッドを使用しているため、必要なスレッドがすでに作成されている可能性があります。

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

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

また、開始する前にあなたが持っているアイテムの数がわからないので、私はループではなくParallel.ForEachを使用することをお勧めします。あなたがいる状況に合わせて設計されているだけなので、ヒューリスティックがよりよく適用されます。 (より洗練されたコードの作成にもなります)

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()); 
+0

実際には、 'queue.CompleteAdding()'をいつ呼び出すのか分からないので、実際にはアプローチができないようです。これは、キューが空であり、誰も他のアイテムで作業していない場合のみです。 –

+0

@FrankRazenberg Nope。追加する項目がなくなると、 'CompleteAdding'を呼び出します。それが空であるのを待つ必要はなく、作業中のアイテムがなくなるまで待つ必要はありません。 'BlockingCollection'はすでにそれを処理します。 'CompleteAdding'は単に列挙子が内部コレクションにそれ以上のアイテムを追加しないようにするため、最終的にはブロックしてより多くのアイテムを待つのではなく、最後に破棄する必要があります。 – Servy

+0

しかし、いつ/どこでCompleteAdding()を呼び出すのかを知っていますか?それは一度だけ呼び出すことができます。 –

関連する問題