平行 LINQ (PLINQ) 是 Language-Integrated 查詢模式 的平行實作。 PLINQ 實作一組完整的 LINQ 標準查詢運算子做為命名空間的 System.Linq 擴充方法,並具有平行作業的其他運算符。 PLINQ 結合了 LINQ 語法的簡單性和可讀性,以及平行程式設計的強大功能。
小提示
如果您不熟悉 LINQ,它會提供統一的模型,以安全類型的方式查詢任何可列舉的數據源。 LINQ to Objects 是針對記憶體內部集合執行之 LINQ 查詢的名稱,例如 List<T> 和陣列。 本文假設您已對 LINQ 有基本的瞭解。 如需詳細資訊,請參閱 Language-Integrated Query (LINQ) 。
什麼是平行查詢?
PLINQ 查詢在許多方面類似於非平行 LINQ to Objects 查詢。 PLINQ 查詢,就像循序 LINQ 查詢一樣,在任何記憶體 IEnumerable 內部或 IEnumerable<T> 數據源上運作,並延遲執行,這表示在列舉查詢之前,它們不會開始執行。 主要差異在於 PLINQ 嘗試充分利用系統上的所有處理器。 其方式是將數據源分割成區段,然後在多個處理器上平行地在每個區段上執行查詢。 在許多情況下,平行執行表示查詢的執行速度會大幅加快。
透過平行執行,PLINQ 可以針對特定類型的查詢,透過舊版程式代碼達到顯著的效能改善,通常只要將 AsParallel 查詢作業新增至數據源即可。 不過,平行處理原則可能會帶來自己的複雜性,並非所有查詢作業在 PLINQ 中執行得更快。 事實上,平行處理實際上會降低特定查詢的速度。 因此,您應該瞭解排序等問題如何影響平行查詢。 如需詳細資訊,請參閱 瞭解 PLINQ 中的加速。
備註
這份文件使用 lambda 運算式來在 PLINQ 中定義委派。 如果您不熟悉 C# 或 Visual Basic 中的 Lambda 運算式,請參閱在 PLINQ 和 TPL 中Lambda 運算式。
本文的其餘部分提供主要 PLINQ 類別的概觀,並討論如何建立 PLINQ 查詢。 每個區段都包含更詳細的信息和程式碼範例連結。
ParallelEnumerable 類別
類別 System.Linq.ParallelEnumerable 會公開幾乎所有PLINQ的功能。 它和命名空間類型的其餘部分 System.Linq 會編譯成 System.Core.dll 元件。 Visual Studio 中的預設 C# 和 Visual Basic 專案都會參考程式集並匯入命名空間。
ParallelEnumerable 包含 LINQ to Objects 所支援之所有標準查詢運算子的實作,但不會嘗試平行處理每個運算符。 如果您不熟悉 LINQ,請參閱 LINQ 簡介 (C#) 和 LINQ 簡介 (Visual Basic)。
除了標準查詢運算符之外,類別 ParallelEnumerable 還包含一組方法,可啟用平行執行的特定行為。 下表列出這些PLINQ特定方法。
ParallelEnumerable 運算子 | 說明 |
---|---|
AsParallel | PLINQ 的進入點。 指定如果可能的話,應該平行處理查詢的其餘部分。 |
AsSequential | 指定應該循序執行查詢的其餘部分,做為非平行 LINQ 查詢。 |
AsOrdered | 指定 PLINQ 應該保留其餘查詢的來源序列排序,或直到排序變更為止,例如使用 orderby (Visual Basic 中的 Order By) 子句。 |
AsUnordered | 指定 PLINQ 在其餘的查詢中,不需要保留來源序列的順序。 |
WithCancellation | 指定 PLINQ 應該定期監視所提供取消令牌的狀態,並在要求時取消執行。 |
WithDegreeOfParallelism | 指定 PLINQ 應該用來平行處理查詢的處理器數目上限。 |
WithMergeOptions | 提供有關 PLINQ 在可能的情況下,如何將平行運算的結果回合併為單一序列,然後傳回給消費線程的提示。 |
WithExecutionMode | 指定即使預設行為是循序執行查詢,PLINQ 還是應該平行處理查詢。 |
ForAll | 多線程列舉方法,與逐一查看查詢結果不同,可讓結果平行處理,而不需要先合併回取用者線程。 |
Aggregate 超載 | PLINQ 的一個獨特的重載功能,可以在線程本地分割上進行中間聚合,並使用最終聚合函式來合併所有分割區的結果。 |
選擇加入機制
當您撰寫查詢時,請在數據源上叫用 ParallelEnumerable.AsParallel 擴充方法,以選擇加入 PLINQ,如下列範例所示。
var source = Enumerable.Range(1, 10000);
// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
where num % 2 == 0
select num;
Console.WriteLine($"{evenNums.Count()} even numbers out of {source.Count()} total");
// The example displays the following output:
// 5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)
' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
Where num Mod 2 = 0
Select num
Console.WriteLine("{0} even numbers out of {1} total",
evenNums.Count(), source.Count())
' The example displays the following output:
' 5000 even numbers out of 10000 total
擴充方法會將 AsParallel 後續的查詢運算符,在此案例中, where
和 select
系結至 System.Linq.ParallelEnumerable 實作。
執行模式
根據預設,PLINQ 是保守的。 在運行時間,PLINQ 基礎結構會分析查詢的整體結構。 如果查詢可能會透過平行處理來產生加速,PLINQ 會將來源序列分割成可以同時執行的工作。 如果平行處理查詢並不安全,PLINQ 只會循序執行查詢。 如果 PLINQ 在可能昂貴的平行演算法或廉價的循序演算法之間有選擇,則預設會選擇循序演算法。 您可以使用 WithExecutionMode 方法和 System.Linq.ParallelExecutionMode 列舉,指示 PLINQ 選取平行演算法。 當您透過測試與測量確定特定查詢在平行方式下執行更快速時,這會很有用。 如需詳細資訊,請參閱 如何:在 PLINQ 中指定執行模式。
並行度
根據預設,PLINQ 會使用主計算機上的所有處理器。 您可以使用 WithDegreeOfParallelism 方法指示 PLINQ 僅使用不超過所指定數目的處理器。 當您想要確定計算機上執行的其他進程收到一定數量的CPU時間時,這會很有用。 下列代碼段會限制查詢使用最多兩個處理器。
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
where Compute(item) > 42
select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
Where Compute(item) > 42
Select item
如果查詢執行大量非計算密集的工作,例如檔案 I/O,則指定大於計算機上核心數目的平行度可能會有所幫助。
已排序與未排序的平行查詢
在某些查詢中,查詢運算符必須產生結果,以保留來源序列的順序。 PLINQ 會針對此目的提供 AsOrdered 運算符。 AsOrdered 與 AsSequential不同。 序列 AsOrdered 仍會以平行方式處理,但其結果會經過緩衝處理並排序。 由於順序保留通常牽涉到額外的工作,AsOrdered 序列的處理速度可能會比預設 AsUnordered 序列慢。 特定排序的平行作業是否比作業的循序版本快,取決於許多因素。
下列程式代碼範例示範如何選擇保留順序。
var evenNums =
from num in numbers.AsParallel().AsOrdered()
where num % 2 == 0
select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
Where num Mod 2 = 0
Select num
如需詳細資訊,請參閱 PLINQ 中的訂單保留。
平行查詢與循序查詢
某些作業需要以循序方式傳遞源數據。 當需要時,ParallelEnumerable 查詢運算子會自動切換為循序模式。 針對需要循序執行的使用者定義查詢運算元和使用者委派,PLINQ 會提供 AsSequential 方法。 當您使用 AsSequential時,查詢中的所有後續運算子都會循序執行,直到 AsParallel 再次呼叫為止。 如需詳細資訊,請參閱 如何:合併平行和循序 LINQ 查詢。
合併查詢結果的選項
當 PLINQ 查詢以平行方式執行時,必須將每個工作執行緒的結果合併回主執行緒,以供 foreach
迴圈使用(For Each
在 Visual Basic 中),或插入清單或陣列。 在某些情況下,指定特定類型的合併作業可能很有説明,例如,更快速地開始產生結果。 為了達到此目的,PLINQ 支援 WithMergeOptions 方法和 ParallelMergeOptions 列舉。 如需詳細資訊,請參閱 PLINQ 中的合併選項。
ForAll 運算子
在循序 LINQ 查詢中,執行會延後,直到查詢在foreach
(在 Visual Basic 是For Each
)迴圈中列舉,或是叫用方法如ToList、ToArray或ToDictionary。 在 PLINQ 中,您也可以使用 foreach
來執行查詢並逐一查看結果。 不過, foreach
本身不會平行執行,因此,它需要將所有平行工作的輸出合併回執行循環的線程。 在 PLINQ 中,當您必須保留查詢結果的最終順序時,或是當您以序列方式處理結果時,例如在每個元素上呼叫 foreach
時,您可以使用 Console.WriteLine
。 若要在不需要順序保留時以及處理結果本身可以平行處理時,更快速地執行查詢,請使用 ForAll 方法來執行 PLINQ 查詢。
ForAll 不會執行這個最終的合併步驟。 下列程式代碼範例示範如何使用 ForAll 方法。
System.Collections.Concurrent.ConcurrentBag<T> 在此使用,是因為它已經過優化以處理多個線程同時新增任務,而無需嘗試移除任何項目。
var nums = Enumerable.Range(10, 10000);
var query =
from num in nums.AsParallel()
where num % 10 == 0
select num;
// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll(e => concurrentBag.Add(Compute(e)));
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
Where num Mod 10 = 0
Select num
' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))
下圖顯示foreach
與ForAll在查詢執行方面的差異。
取消
PLINQ 與 .NET 中的取消類型整合。 (如需詳細資訊,請參閱 Managed 線程中的取消。)因此,不同於循序 LINQ to Objects 查詢,PLINQ 查詢可以取消。 若要建立可取消的 PLINQ 查詢,請在 WithCancellation 查詢上使用 運算符,並提供 CancellationToken 實例作為自變數。 IsCancellationRequested當 token 上的屬性設定為 true 時,PLINQ 會注意到此變更,停止處理所有執行緒,並擲回 OperationCanceledException。
PLINQ 查詢可能會在設定取消標記之後繼續處理某些元素。
為了獲得更高的回應效率,您也可以回應長時間執行的使用者委派中的取消要求。 如需詳細資訊,請參閱 如何:取消 PLINQ 查詢。
例外狀況
當 PLINQ 查詢執行時,可能會同時從不同的線程擲回多個例外狀況。 此外,處理例外狀況的程式代碼可能位於與擲回例外狀況的程式代碼不同的線程上。 PLINQ 使用 AggregateException 類型來封裝查詢擲回的所有例外狀況,並將這些例外狀況傳遞回呼叫執行緒。 在呼叫線程上,僅需要一個 try-catch 區塊。 不過,您可以逐一遍歷在AggregateException中封裝的所有例外狀況,並攔截任何可安全地從中復原的例外狀況。 在罕見的情況下,有些例外可能會拋出而未包裹在AggregateException中,而ThreadAbortException也不會包裹。
當允許例外狀況反升回聯結線程時,查詢可能會在引發例外狀況之後繼續處理某些專案。
如需詳細資訊,請參閱 如何:處理 PLINQ 查詢中的例外狀況。
自定義分割器
在某些情況下,您可以藉由撰寫利用源數據的某些特性的自定義分割器來改善查詢效能。 在查詢中,自定義分割器本身是可查詢的可列舉物件。
int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
PLINQ 支援固定數目的數據分割(雖然數據可能會在運行時間動態地重新指派給這些分割區,以進行負載平衡。 For 和 ForEach 僅支援動態數據分割,這表示運行時間的數據分割數目會變更。 如需詳細資訊,請參閱 PLINQ 和 TPL 的自定義分割器。
測量 PLINQ 效能
在許多情況下,可以平行處理查詢,但設定平行查詢的額外負荷超過取得的效能優勢。 如果查詢沒有執行太多計算,或數據源很小,PLINQ 查詢可能會比循序 LINQ to Objects 查詢慢。 您可以使用 Visual Studio Team Server 中的平行效能分析器來比較各種查詢的效能、找出處理瓶頸,以及判斷您的查詢是以平行或循序方式執行。 如需詳細資訊,請參閱 並行可視化檢視 和 如何:測量 PLINQ 查詢效能。