2012-10-25 28 views
6

我想了解爲什麼在以下場景中Parallel.For能夠勝過多個線程:考慮可以並行處理的一批作業。在處理這些工作時,可能會添加新工作,然後需要處理。該Parallel.For的解決辦法如下所示:Parallel.For vs普通線程

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

這兩個代碼示例並不相同。

Parallel.ForEach()將使用有限的線程並重新使用它們。第二個示例已經開始創建一些線程。這需要時間。

什麼是maxThreads的值?非常關鍵,在Parallel.ForEach()它是動態的。

任務調度程序就是這麼好嗎?

這很不錯。 TPL使用偷工減料和其他自適應技術。你會很難做得更好。

+0

線程示例重新使用它創建的線程。它啓動的數量有限,如果這就是你的意思,那麼每個作業都不會有一個。 –

+0

把我打倒吧,使用線程池不是。 http://stackoverflow.com/questions/230003/thread-vs-threadpool –

+0

@Justin:阿哈,很好的參考。謝謝。 –

1

您創建了一堆新線程,並且Parallel.For使用了一個線程池。如果你使用C#線程池,你會看到更好的性能,但實際上沒有意義。

我會迴避推出自己的解決方案;如果有需要定製的角落案例,請使用TPL並自定義。

3

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

實際上,似乎這種方法不可行,因爲你不知道什麼時候調用'queue.CompleteAdding()'。這隻有當隊列都是空的並且沒有人正在處理更多項目時。 –

+0

@FrankRazenberg沒有。當沒有更多項目要添加時,您只需調用'CompleteAdding'。您不需要等待它爲空或者沒有更多項目處於正在處理的過程中。 'BlockingCollection'已經處理好了。 'CompleteAdding'只是意味着枚舉器不會添加任何更多的項目到它的內部集合中,所以當它最終拋出最後一個時它應該中斷,而不是阻塞和等待更多項目。 – Servy

+0

但是,您如何知道何時/在哪裏調用CompleteAdding()?它只能被調用一次,對吧? –