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 查詢一樣,會對記憶體中的 IEnumerableIEnumerable<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 擴充方法會將後續的查詢運算子 (此案例中為 whereselect) 繫結至 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 不同於 AsSequentialAsOrdered 序列仍會以平行方式處理,但其結果會新增到緩衝區並加以排序。 順序保留通常涉及額外工作,因此,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) 迴圈中列舉查詢或叫用 ToListToArrayToDictionary 之類的方法為止才會執行查詢。 在 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)))

下圖顯示 foreachForAll 兩者在執行查詢方面的差異。

ForAll vs. ForEach

取消

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 支援固定數目的分割 (但在執行階段為了保持負載平衡,系統可能會以動態方式將資料重新指派給這些分割)。 ForForEach 僅支援動態分割,這表示分割區數目是在執行階段變更的。 如需詳細資訊,請參閱 PLINQ 和 TPL 的自訂 Partitioner

測量 PLINQ 效能

在許多情況下,查詢可平行處理,但設定平行查詢時所帶來的額外負荷,遠超過所獲得的效能好處。 如果查詢不會執行許多計算,或如果資料來源很小,PLINQ 查詢的速度可能會比 LINQ to Objects 循序查詢還慢。 您可以使用 Visual Studio Team Server 中的 Parallel Performance Analyzer 來比較各種查詢的效能,以找出處理瓶頸,以及判斷您的查詢該平行執行還是循序執行。 如需詳細資訊,請參閱並行視覺化檢視如何:測量 PLINQ 查詢效能

另請參閱