讓我們考慮一個簡單的編程挑戰:對大數組中的所有元素求和。現在可以通過使用並行性來輕松優化這一點,特別是對於具有數千或數百萬個元素的巨大陣列,還有理由認為,並行處理時間應該與常規時間除以CPU核心數一樣多。事實證明,這一壯舉並不容易實現。我將向您展示幾種並行執行此操作的方法,它們如何改善或降低性能以及以某種方式影響性能的所有細節。
簡單的循環方法
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的方法,研究性能是否可以更快。
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還可以繼續優化,也許可以更快
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優化過的差不多,有些時候更快
避免在循環中使用Task.Run
您可以在要執行並發活動時使用任務,如果您需要高度的並行性,任務永遠不是一個好的選擇,始終建議避免在ASP.Net中使用線程池線程。因此,您應該避免在ASP.Net中使用Task.Run或Task.factory.StartNew。
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更有效,因為它將通過在系統中的多個核心之間分配工作負載來優化執行。
結論和總結
並行化優化肯定可以提高性能,但是這取決於很多因素,每個案例都應該進行測量和檢查。
當各種線程需要通過某種鎖定機制相互依賴時,性能會顯着降低。
50萬數據運行結果
其他的多線程文章
2. .NET中並行開發優化
3. C# Task.Run 和 Task.Factory.StartNew 區別
4. C#中多線程的並行處理
5. C#中多線程中變量研究