讓咱們考慮一個簡單的編程挑戰:對大數組中的全部元素求和。如今能夠經過使用並行性來輕鬆優化這一點,特別是對於具備數千或數百萬個元素的巨大陣列,還有理由認爲,並行處理時間應該與常規時間除以CPU核心數同樣多。事實證實,這一壯舉並不容易實現。我將向您展現幾種並行執行此操做的方法,它們如何改善或下降性能以及以某種方式影響性能的全部細節。html
private const int ITEMS = 500000; private int[] arr = null; public ArrayC() { arr = new int[ITEMS]; var rnd = new Random(); for (int i = 0; i < ITEMS; i++) { arr[i] = rnd.Next(1000); } } public long ForLocalArr() { long total = 0; for (int i = 0; i < ITEMS; i++) { total += int.Parse(arr[i].ToString()); } return total; } public long ForeachLocalArr() { long total = 0; foreach (var item in arr) { total += int.Parse(item.ToString()); } return total; }
只須要迭代循環就能夠計算出結果,超級簡單,這裏沒有用直接相加求出結果,緣由是直接求出結果,發現每次基本的運行都比並行快,可是實際上,並行處理沒有那麼簡單,因此這裏的加法就簡單的處理下total += int.Parse(arr[i].ToString())。如今,讓咱們嘗試用並行性來戰勝數組迭代吧。編程
private object _lock = new object(); public long ThreadPoolWithLock() { long total = 0; int threads = 8; var partSize = ITEMS / threads; Task[] tasks = new Task[threads]; for (int iThread = 0; iThread < threads; iThread++) { var localThread = iThread; tasks[localThread] = Task.Run(() => { for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++) { lock (_lock) { total += arr[j]; } } }); } Task.WaitAll(tasks); return total; }
請注意,您必須使用localThread變量來「保存」該iThread時間點的值。不然,它將是一個隨着for循環前進而變化的捕獲變量。當數據最後打的時候並行已經比普通的快了,可是發現快的很少,說明還能夠優化數組
public long ThreadPoolWithLock2() { long total = 0; int threads = 8; var partSize = ITEMS / threads; Task[] tasks = new Task[threads]; for (int iThread = 0; iThread < threads; iThread++) { var localThread = iThread; tasks[localThread] = Task.Run(() => { long temp = 0; for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++) { temp += int.Parse(arr[j].ToString()); } lock (_lock) { total += temp; } }); } Task.WaitAll(tasks); return total; }
增長設置臨時變量,減小lock次數,發現運行效果已經有質的提升,提升了幾倍。突然想起,有個Parallel.For的方法,研究性能是否能夠更快。多線程
public long ParallelForWithLock() { long total = 0; int parts = 8; int partSize = ITEMS / parts; var parallel = Parallel.For(0, parts, new ParallelOptions(), (iter) => { long temp = 0; for (int j = iter * partSize; j < (iter + 1) * partSize; j++) { temp += int.Parse(arr[j].ToString()); } lock (_lock) { total += temp; } }); return total; }
運行結果比普通迭代快,可是沒有ThreadPool快,可是以爲Parallel.For還能夠繼續優化,也許能夠更快併發
public long ParallelForWithLock2() { long total = 0; int parts = 8; int partSize = ITEMS / parts; var parallel = Parallel.For(0, parts, localInit: () => 0L, // Initializes the "localTotal" body: (iter, state, localTotal) => { for (int j = iter * partSize; j < (iter + 1) * partSize; j++) { localTotal += int.Parse(arr[j].ToString()); } return localTotal; }, localFinally: (localTotal) => { total += localTotal; }); return total; }
運行效果已經很快,和ThreadPool優化過的差很少,有些時候更快dom
您能夠在要執行併發活動時使用任務,若是您須要高度的並行性,任務永遠不是一個好的選擇,始終建議避免在ASP.Net中使用線程池線程。所以,您應該避免在ASP.Net中使用Task.Run或Task.factory.StartNew。async
Task.Run應始終用於CPU綁定代碼。Task.Run在ASP.Net應用程序或利用ASP.Net運行時的應用程序中不是一個好選擇,由於它只是將工做卸載到ThreadPool線程。若是您使用的是ASP.Net Web API,則該請求已經使用了ThreadPool線程。所以,若是在ASP.Net Web API應用程序中使用Task.Run,則只是經過將工做卸載到另外一個工做線程來限制可伸縮性。性能
請注意,在循環中使用Task.Run存在缺點。若是在循環中使用Task.Run方法,則會建立多個任務 - 每一個工做單元或迭代一個任務。可是,若是使用Parallel.ForEach代替在循環中使用Task.Run,則會建立分區程序以免建立更多任務來執行活動而不是須要它。這可能會顯着提升性能,由於您能夠避免過多的上下文切換,而且仍然能夠利用系統中的多個內核。優化
應該注意的是,Parallel.ForEach在內部使用Partitioner <T>,以便將集合分發到工做項中。順便說一句,這種分發不會發生在項目列表中的每一個任務中,而是做爲批處理髮生。這下降了所涉及的開銷,從而提升了性能。換句話說,若是在循環中使用Task.Run或Task.Factory.StartNew,它們將爲循環中的每次迭代顯式建立新任務。Parallel.ForEach更有效,由於它將經過在系統中的多個核心之間分配工做負載來優化執行。spa
並行化優化確定能夠提升性能,可是這取決於不少因素,每一個案例都應該進行測量和檢查。
當各類線程須要經過某種鎖定機制相互依賴時,性能會顯着下降。
2. .NET中並行開發優化
3. C# Task.Run 和 Task.Factory.StartNew 區別
4. C#中多線程的並行處理
5. C#中多線程中變量研究