我在業餘時間開發維護了一款免費開源的升訊威線上客服系統,也收穫了許多使用者。對我來說,只要能獲得使用者的認可,就是我最大的動力。
最近客服系統成功經受住了客戶現場組織的壓力測試,獲得了客戶的認可。
客戶組織多名客服上線後,所有員工同一時間開啟訪客頁面瘋狂不停的給線上客服發訊息,系統穩定無異常無掉線,客服回覆訊息正常。訊息實時到達無任何延遲。
我會通過一系列的文章詳細分析升訊威線上客服系統的並行高效能技術是如何實現的,使用了哪些方案以及具體的做法。
本篇介紹 PLINQ 並行查詢技術。
並行 LINQ (PLINQ) 是語言整合查詢 (LINQ) 模式的並行實現。 PLINQ 將整套 LINQ 標準查詢運運算元實現為 System.Linq 名稱空間的擴充套件方法,並提供適用於並行操作的其他運運算元。 PLINQ 將 LINQ 語法的簡潔和可靠性與並行程式設計的強大功能結合在一起。
一個 PLINQ 查詢的許多方面都類似於非並行的 LINQ to Objects 查詢。 與順序 LINQ 查詢一樣,PLINQ 查詢對任何記憶體中 IEnumerable 或 IEnumerable
通過並行執行,通常只需向資料來源新增 AsParallel 查詢操作,PLINQ 即可顯著提升效能(與某些型別查詢的舊程式碼相比)。 但是,並行可能會引入其自身的複雜性,因此並非所有的查詢操作的執行速度在 PLINQ 中都更快。 事實上,並行實際上會降低某些查詢的速度。 因此,應瞭解排序等問題將如何對並行查詢產生影響。
下面各部分列出了並行查詢效能的一些最重要的影響因素。 這些都是一般性說明,本身並不足以用於在所有情況下預測查詢效能。
var queryA = from num in numberList.AsParallel()
select ExpensiveFunction(num); //good for PLINQ
var queryB = from num in numberList.AsParallel()
where num % 2 > 0
select num; //not as good for PLINQ
系統上的邏輯核心數量(並行度)。
這一點是上一部分的必然結果,在具有更多核心的計算機上,適合並行查詢執行得更快,這是因為可以在更多並行執行緒之間劃分工作。 加速總量取決於查詢整體工作的並行度百分比。 不過,不要認為所有查詢在八核計算機上的執行速度都是在四核計算機上的兩倍。 優化查詢以實現最佳效能時,請務必在具有不同數量核心的計算機上度量實際結果。 這一點與第 1 點相關:需要更大的資料集,才能利用更多的計算資源。
操作的數量和種類。
如果有必要維護源序列中的元素順序,PLINQ 提供 AsOrdered 運運算元。 雖然排序有相關成本,但此成本通常還算低。 GroupBy 和 Join 操作同樣也會產生開銷。 如果允許按任意順序處理源集合中的元素,並在準備就緒後立即將它們傳遞給下一個運運算元,PLINQ 的效能最佳。
查詢執行形式。
若要通過呼叫 ToArray 或 ToList 儲存查詢結果,所有並行執行緒的結果都必須合併到一個資料結構中。 這就涉及不可避免的計算成本。 同樣,如果使用 foreach(Visual Basic 中的 For Each)迴圈來回圈存取結果,工作執行緒的結果必須序列化到列舉元執行緒。 不過,如果只想根據每個執行緒的結果執行某操作,可以使用 ForAll 方法對多個執行緒執行此操作。
合併選項型別。
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
AsParallel 擴充套件方法將後續查詢運運算元(在此範例中為 where 和 select)繫結到 System.Linq.ParallelEnumerable 實現。
預設情況下,PLINQ 是保守的。 在執行時,PLINQ 基礎結構將分析查詢的總體結構。 如果通過並行可能會提高查詢速度,PLINQ 則將源序列分割區為可以同時執行的任務。 如果並行化查詢不安全,PLINQ 則只會按順序執行查詢。 如果 PLINQ 可以在可能會較昂貴的並行演演算法或成本較低的順序演演算法之間進行選擇,它會預設選擇順序演演算法。 可以使用 WithExecutionMode 方法和 System.Linq.ParallelExecutionMode 列舉指示 PLINQ 選擇並行演演算法。 如果你通過測試和測量知道特定查詢以並行方式執行得更快時,此做法非常有用。
預設情況下,PLINQ 使用主機計算機上的所有處理器。 可以使用 WithDegreeOfParallelism 方法指示 PLINQ 使用不超過指定數量的處理器。 當你要確保計算機上執行的其他程序收到一定的 CPU 時間量時,此做法將非常有用。 下面的片段將查詢限制為最多使用兩個處理器。
var 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;
某些操作要求按順序提供源資料。 必要時,ParallelEnumerable 查詢運運算元自動還原為順序模式。 對於要求順序執行的使用者定義的查詢運運算元和使用者委託,PLINQ 提供了 AsSequential 方法。 使用 AsSequential 時,查詢中的所有後續運運算元都會順序執行,直到再次呼叫 AsParallel。
當一個 PLINQ 查詢執行時,可能會同時從不同的執行緒引發多個異常。 此外,處理異常的程式碼可能與引發異常的程式碼處於不同的執行緒上。 PLINQ 使用 AggregateException 型別封裝查詢丟擲的所有異常,並將這些異常封送回撥用執行緒。 在呼叫執行緒上,只需要一個 try-catch 塊。 不過,可以迴圈存取在 AggregateException 中封裝的所有異常,並捕獲任何可以安全恢復的異常。 在極少數情況下,可能會丟擲一些未在 AggregateException 中包裝、ThreadAbortException 也沒有進行包裝的異常。
如果允許異常向上冒泡回到聯接執行緒,則查詢也許可以在引發異常後繼續處理一些項。
在某些情況下,可以通過編寫利用源資料的某些特徵的自定義分割區程式來提高查詢效能。 在查詢中,自定義分割區程式本身是被查詢的可列舉物件。
int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
PLINQ 支援固定數量的分割區(儘管在執行時期間為了負載均衡可能會將資料重新動態分配到這些分割區)。 For 和 ForEach 僅支援動態分割區。也就是說,分割區數在執行時發生變化。
在順序 LINQ 查詢中,執行一直延遲到在 foreach(Visual Basic 中為 For Each)迴圈中或通過呼叫 ToList、ToArray 或 ToDictionary 等方法列舉查詢。 在 PLINQ 中,還可以使用 foreach 執行查詢以及迴圈存取結果。 但是,foreach 本身不會並行執行,因此,它要求將所有並行任務的輸出合併回該回圈正在上面執行的執行緒中。 在 PLINQ 中,在必須保留查詢結果的最終排序,以及以按序列方式處理結果時,例如當為每個元素呼叫 Console.WriteLine 時,則可以使用 foreach。 為了在無需順序暫留以及可自行並行處理結果時更快地執行查詢,請使用 ForAll 方法執行 PLINQ 查詢。 ForAll 不執行最終的這一合併步驟。 下面的程式碼範例說明如何使用 ForAll 方法。 此處使用 System.Collections.Concurrent.ConcurrentBag
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)));
在很多情況下,可以並行化查詢,但是設定並行查詢的開銷可能會超出獲得的效能收益。 如果查詢不執行大量的計算,或者如果資料來源較小,則 PLINQ 查詢的速度可能比順序 LINQ to Objects 查詢的速度慢。 可以在 Visual Studio Team Server 中使用並行效能分析器比較各種查詢的效能,查詢處理瓶頸,以及確定查詢是並行執行還是按順序執行。
升訊威線上客服與行銷系統是一款客服軟體,但更重要的是一款行銷利器。