PLINQ 簡介
平行 LINQ (PLINQ) 是 Language-Integrated Query (LINQ) 模式的平行實作。 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("{0} even numbers out of {1} total",
evenNums.Count(), source.Count());
// 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
迴圈 (在 Visual Basic 中是 For Each
) 取用,或以供插入至清單或陣列。 在某些情況下,指定特定種類的合併作業可能會有幫助,例如,若您要更快速地開始產生結果的話。 基於此目的,PLINQ 支援 WithMergeOptions 方法與 ParallelMergeOptions 列舉。 如需詳細資訊,請參閱 PLINQ 中的合併選項。
ForAll 運算子
在循序 LINQ 查詢中,直到在 foreach
(Visual Basic 中為 For Each
) 迴圈中列舉查詢或叫用 ToList、ToArray 或 ToDictionary 之類的方法為止才會執行查詢。 在 PLINQ 中,您也可以使用 foreach
來執行查詢,並逐一查看各項結果。 不過,foreach
本身並不會以平行方式執行,因此,所有平行工作的輸出必須合併回用來執行迴圈的執行緒。 在 PLINQ 中,當您必須保留查詢結果的最終排序時,以及每當您以序列方式處理結果時 (例如,當您為每個元素呼叫 Console.WriteLine
時),您都可以使用 foreach
。 在不必保留順序時,以及在結果本身可以平行處理時,若您需要更快速地執行查詢,請使用 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 屬性設為 true 時,PLINQ 將注意到,並會停止所有執行緒上的處理作業,然後擲回 OperationCanceledException。
在設定了取消權杖之後,PLINQ 查詢還是可能會繼續處理某些元素。
若要提高回應速度,您也可以在長時間執行的使用者委派中回應取消要求。 如需詳細資訊,請參閱如何:取消 PLINQ 查詢。
例外狀況
PLINQ 查詢在執行時,不同的執行緒可能會同時擲回多個例外狀況。 此外,負責處理例外狀況之程式碼所在的執行緒,可能會不同於擲回例外狀況之程式碼所在的執行緒。 PLINQ 會使用 AggregateException 型別將查詢擲回的所有例外狀況封裝起來,然後將這些例外狀況封送處理回呼叫端執行緒。 呼叫端執行緒只需要一個 try-catch 區塊。 不過,您可以逐一查看 AggregateException 中封裝的所有例外狀況,並攔截您可以從中安全復原的任何例外狀況。 在少數情況下,某些例外狀況擲回時可能未包裝於 AggregateException 中,ThreadAbortException 也未包裝。
當系統允許例外狀況反昇至聯結的執行緒時,查詢可能就可以在引發例外狀況之後,繼續處理某些項目。
如需詳細資訊,請參閱如何:處理 PLINQ 查詢中的例外狀況。
自訂 Partitioner
在某些情況下,您可以藉由撰寫自訂 Partitioner 來利用來源資料的某些特性,以改善查詢效能。 在查詢中,自訂 Partitioner 本身就是所查詢的可列舉物件。
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 的自訂 Partitioner。
測量 PLINQ 效能
在許多情況下,查詢可平行處理,但設定平行查詢時所帶來的額外負荷,遠超過所獲得的效能好處。 如果查詢不會執行許多計算,或如果資料來源很小,PLINQ 查詢的速度可能會比 LINQ to Objects 循序查詢還慢。 您可以使用 Visual Studio Team Server 中的 Parallel Performance Analyzer 來比較各種查詢的效能,以找出處理瓶頸,以及判斷您的查詢該平行執行還是循序執行。 如需詳細資訊,請參閱並行視覺化檢視和如何:測量 PLINQ 查詢效能。