PLINQ (Parallel LINQ) 是 .Net 4.0 開始引進的新功能之一, 其主要目的是提供 LINQ to Object 的平行處理支援, 所以(理論上)可以提高整體運算速度。PLINQ 針對暫存在記憶體中的物件(包括集合物件、陣列等等)提供查詢功能, 這點和傳統的 LINQ 查詢一樣, 不過 PLINQ 會把工作自動分配給許多個工作執行緒 (Working Threads), 藉此提高執行效率。如果 .Net 查覺到有些工作可能不適合或不需要做平行處理, 那麼它會自動採用循序處理的方式進行(如同採用傳統的 LINQ 一樣)...
平行處理 (Parallel Computing) 以及 PLINQ (Parallel LINQ) 是 .Net 4.0 開始引進的新功能。後者的主要目的是藉由平行運算的新功能, 提供 LINQ to Object 的平行處理支援, 所以(理論上)可以提高整體運算速度。PLINQ 針對暫存在記憶體中的物件 (包括集合物件、陣列等等) 提供查詢功能, 這點和傳統的 LINQ 查詢一樣, 不過 PLINQ 會把工作自動分配給多個工作執行緒 (Working Threads), 藉此提高執行效率。不過, 如果 .Net 查覺到有些工作可能不適合或不需要做平行處理, 那麼它會自動採用循序處理的方式進行 (如同採用傳統的 LINQ 一樣)。
基本概念
在一些特別的情境下, PLINQ 可以鉅幅提升執行效率; 不過在絕大部份情況下, 我們感受不到它的好處, 甚至可能比使用普通的 LINQ 查詢還要慢。這個特性大致上和平行處理的理論是一樣的。所以, 千萬不要對 PLINQ 抱持過度不切實際的幻想。
PLINQ 的功能都位於 System.Linq.ParallelEnumerable 類別之下。以下, 我們直接來看 PLINQ 的用法:
using System.IO; using System.Collections.Generic; using System.Linq; using System.Linq.Parallel; using System.Threading; ... IEnumerable<string> files = Directory.EnumerateFiles(@"C:\", "*.*", SearchOption.AllDirectories); IEnumerable<string> results = from f in files.AsParallel() select f; IList<string> resultsList = results.ToList(); Console.WriteLine("Found {0} files.", resultsList.Count());
在上述程式中, 把 .AsParallel() 加上去就變成 PLINQ 陣述式, 把 .AsParallel() 拿掉就變成原始的 LINQ 陣述式。在 LINQ 和 PLINQ 之間, 並沒有什麼型別轉換動作需要我們自己去動手。
前面說過, 即使你採用了 PLINQ 陣述式, 它實際上也不一定自動分配多執行緒。不過事實上你可以強迫 PLINQ 採用平行處理, 那就是下達 .WithExecutionMode(ParallelExecutionMode.ForceParallelism) 子句, 如下例:
from f in files.AsParallel()
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
select f;
到底應不應該強迫 PLINQ 採用平行處理, 這不是憑感覺來下決定的; 我們最好能先做實驗, 取得採用循序處理所得到的實際數據, 再取得採用平行處理的實際數據, 兩相比較之後, 再做決定也不遲。在實務情況下, 或許 .Net 會替你決定查詢採用循序方式, 但如果你確定該查詢僅在偶然情況下會突然出現需要大量運算的狀況, 若不強迫採用平行運算會導致極冗長的運算時間, 甚至 time-out, 那麼你可能就有必要強迫這段查詢指令使用平行運算。
此外, PLINQ 和 LINQ 一樣, 都會在資料正式被列舉時才會真正執行查詢的動作; 因此, 如果你執行上面的範例程式, 錯誤會出現在
IList<string> resultsList = results.ToList();
這一行, 而不是出現在宣告 PLINQ 查詢的那一行。
中斷機制
和其它一般平行處理的方式一樣, 我們最好能在程式中加入中斷機制, 以免某個耗時的工作發動下去之後沒完沒了。我把第一個範例程式稍為改了一下, 讓它支援中斷的能力:
using System; using System.Collections.Generic; using System.Text; using System.Linq; using System.Threading; using System.Diagnostics; using System.IO; namespace PLINQ_DEMO { class Program { static void Main(string[] args) { string destinationFolder = @"E:\Download"; string fileToFind = "SyncToySetupPackage.exe"; var files = Directory.EnumerateFiles(destinationFolder, "*.*", SearchOption.AllDirectories); CancellationTokenSource cs = new CancellationTokenSource(); Stopwatch sw = Stopwatch.StartNew(); try { var results = from f in files.AsParallel().WithCancellation(cs.Token) select checkFile(f, fileToFind, cs); IList<string> resultsList = results.ToList(); Console.WriteLine("{0} files searched, \"{1}\" is not found.", resultsList.Count(), fileToFind); } catch (OperationCanceledException) { Console.WriteLine("The file \"{0}\" is found!", fileToFind); } finally { sw.Stop(); } Console.WriteLine("Totally it took {0} ms.", sw.ElapsedMilliseconds); Console.ReadKey(); } static string checkFile(string fileName, string fileToCompare, CancellationTokenSource cs) { //Thread.SpinWait(2000000); if (fileName.Trim().Contains(fileToCompare)) cs.Cancel(); return fileName; } } }
在以上程式中, 我加上了 CancellationTokenSource 變數, 由它負責管理旗號(Token); 當程式中發出 CancellationTokenSource.Cancel() 指令時, 即代表整個平行處理作業處理緒都會被取消。同時, 一個 OperationCanceledException 中斷會被發出, 讓我們可以在程式中進行攔截。
在上述程式中, 我對 E:\Downloads 資料夾中所有程式進行查詢, 如果發現裡面有 SyncToySetupPackage.exe 這個程式, 則整個作業中斷, 否則繼續查詢, 直到全部檔案都被查詢過為止。
當然, 這個程式只是做為示範而已, 它實際上應該是用於非常耗時的作業。由於我放置在 E:\Downloads 下的檔案不多 (全部僅 2018 個), 所以不管下去尋找什麼檔案, 都可以在 200ms 之內結束, 而且有找到檔案 (會中斷作業) 比沒有找到檔案 (會把檔案全部列舉出來) 耗時還久, 意思是系統在處理中斷的成本比放著讓它全部跑完還要高。所以, 如果我們不要以為在符合條件時中斷整個平行作業一定會比讓它全部跑完來得節省時間; 這真的要看情況而定。
然而, 如果你把 checkFile() 方法中 Thread.SpinWait(2000000); 這一行的註解拿掉, 使得程式可以模擬耗時的作業, 那麼採用中斷的好處就立即彰顯出來了。同樣採用平行運算, 中斷作業或不中斷作業, 中間的差異可以高達至少上百倍 (E:\Downloads 下的檔案愈多, 差異愈明顯)。
如果某個查詢運算在某種情況下極為耗時 (例如數小時以上), 那麼我們最好能提供使用者手動中斷的功能 (例如讓使用者可以按 Ctrl-C 以取消運算)。做法和上述程式差不多, 你可以參考「How to: Cancel a PLINQ Query」一文所列的程式, 在此我就不引用了。
效能評比
對於多核的處理器, 平行運算可以達到最高的效率。在我的 Core i7 機器上, 如果使用上述程式並開啟平行運算, 四核八緒的 CPU Usage 就會被用到幾乎全滿, 如下圖:
同一個程式, 如果不採用平行運算 (把 .AsParallel 字樣拿掉), 我們就看不到 CPU 滿載的景像了:
雖然如此會使得 CPU 的負載變輕, 然而根據我的記錄, 不使用平行運算的執行時間 (383,838 ms vs. 12,744 ms) 卻比使用平行運算足足多出了 29 倍之多。由此足證在 CPU Bound 的情況下, 平行運算確實有其無可忽視的威力!