使用.NET並行任務庫(TPL)與並行Linq(PLINQ)充分利用多核效能

GuZhenYin發表於2024-09-25
前言

最近比較閒,(專案要轉Java被分到架構組,邊緣化人員,無所事事 哈哈哈哈)

記錄一下前段時間用到的.NET框架下採用並行策略充分利用多核CPU進行最佳化的一個方法

起因是專案中有個結算的方法,需要彙總一個月的資料在記憶體中進行計算,統計,分組 ,然後產生新的資料

在某個客戶那部署後發現,這個方法執行的效率很低,監控發現資料從資料庫查詢出來 很快(因為資料庫單獨一臺伺服器)

然後透過top檢視伺服器的CPU就跑到了100%.記憶體正常,查了下CPU的型號 emm...很爛 但是好在核心很多(畢竟伺服器級的U)..

檢視伺服器核心數 是在16個. Linux用top命令看的話,理論上CPU跑到1600%才算吃滿,但是程式只吃了單個核.

等於1人幹活 15人在吃瓜呀...如圖:

然後檢視了程式碼,發現結算的計算這一塊程式碼是在單個foreach中進行順序計算,所以決定用.NET提供的並行任務庫(TPL)進行最佳化.

最佳化完成後,從之前的結算直接導致執行緒超時異常 變成 大概在20秒左右就結算完成.獲得了巨大的提升.

正文

1 .NET 中的並行程式設計簡介

在硬體發展迅速的今天.有太多的個人電腦和伺服器級CPU都擁有多個 CPU 核心,為了方便多個執行緒能夠同時執行。 充分利用硬體,就可以利用並行程式設計對程式碼進行並行化,以將工作分攤在多個處理器上。

以前,並行化需要自行開啟子執行緒,維護鎖等各種繁瑣操作。但是從 .NET Framework 4 中引入的TPL簡化了並行開發。 我們只需要透過簡單的修改,就可以編寫高效、細化且可伸縮的並行程式碼,而不必直接處理執行緒或執行緒池。

下圖是官方文件的截圖,簡單的說明了 .NET 中的並行程式設計體系結構:

我們可以看到Parallel 就是線上程處理上加了一層封裝好的演算法,讓我們處理並行多執行緒更簡單

2. 並行任務庫(TPL)

任務並行庫 (TPL) 是 System.ThreadingSystem.Threading.Tasks 空間中的一組公共型別和 API。

TPL 的目的是透過簡化將並行和併發新增到應用程式的過程來提高開發人員的工作效率。

TPL 動態縮放併發的程度以最有效地使用所有可用的處理器。

此外,TPL 還處理工作分割槽、ThreadPool 上的執行緒排程、取消支援、狀態管理以及其他低階別的細節操作。

透過使用 TPL,你可以在將精力集中於程式要完成的工作,同時最大程度地提高程式碼的效能。

(以上來自於官方文件,我覺得已經講的很詳細了)

那麼接下來,我們就編寫一個並行任務的示例,來看看效果:

首先,並行任務庫提供了兩個方法 一個Parallel.ForEach 一個Parallel.For 用法都差不多,這裡我們用Parallel.For做實驗

先建立兩個方法,程式碼如下:

 //建立順序執行方法
 static List<dynamic> AddModelSequential(int modelCount)
 {
     var list = new List<dynamic>();
     //為了增加迴圈複雜性,裡面巢狀一個迴圈
     for (int i = 0; i < modelCount; i++)
     {
         int f = 0;
         for (int j = 0; j < 5000; j++)
         {
             f++;
         }
         list.Add(new { bbb = i, aaa = "1", ccc = f });
     }
     return list;
 }
 //建立並行執行方法
 static List<dynamic> AddModelParallel(int modelCount)
 {
     var list = new List<dynamic>();
     Parallel.For(0, modelCount, i =>
     {
         int f = 0;
         //為了增加迴圈複雜性,裡面巢狀一個迴圈
         for (int j = 0; j < 5000; j++)
         {
             f++;
         }
         list.Add(new { bbb = i, aaa = "1",ccc= f});
     });
     return list;
 }

接著執行兩個方法,都跑10W條資料,並記錄執行時間.如下:

 static void Main(string[] args)
 {

     Console.Error.WriteLine("執行順序迴圈...");
     Stopwatch stopwatch = new Stopwatch();
     stopwatch.Start();

     AddModelSequential(1000000);
     stopwatch.Stop();
     Console.Error.WriteLine("順序迴圈時間(毫秒): {0}",
                             stopwatch.ElapsedMilliseconds);

     stopwatch.Reset();
     Console.Error.WriteLine("執行並行迴圈...");
     stopwatch.Start();
     var paradata= AddModelParallel(1000000);
     stopwatch.Stop();
     Console.Error.WriteLine("並行迴圈時間(毫秒): {0}",
                             stopwatch.ElapsedMilliseconds);
     Console.ReadLine();
 }

本人是I9 12代CPU 邏輯處理器有20個,得到結果如圖:

效能提升20倍..

由於在開發機上跑的東西比較多,對於CPU的使用情況,監控不是很清楚,我們掏出..阿里雲99元包郵的2核2G的伺服器..來看看效果.

我們可以明顯看到在2核機上 效能大概也有接近一倍的提升

透過top命令,可以明顯的監聽到CPU的使用情況

在跑第一個迴圈的時候,CPU 100%,單核吃滿,如圖:

跑第二個迴圈的時候,第2顆CPU就開始參與進來了,如圖:

所以在合適的情況下(注意,這裡是合適的情況)

程式中採用並行任務庫充分的利用伺服器的多核效能可以使執行效率有很大的提升.

3. 並行PLINQ

PLINQ 是 LINQ 的一組擴充套件

它允許在執行程式碼的計算機上使用多個處理器或核心對支援 IEnumerable<T> 介面的集合並行執行查詢。

這可以顯著減少處理大型資料集或執行復雜計算所需的時間

注意,這裡可以看到 PLINQ只支援 IEnumerable的介面,所以linq to sql時的表示式樹是不支援的,如果使用則會導致全表查詢到記憶體中

使用方式也很簡單,在資料集處理之前加上AsParallel方法即可,如下:

//LINQ
var results = from item in dataSource
              where item.SomeCondition()
              select item.SomeTransformation();
//PLINQ
var parallelResults = from item in dataSource.AsParallel()
                      where item.SomeCondition()
                      select item.SomeTransformation();

PLINQ的使用場景比較特殊,目前demo中我還沒反映出來比LINQ要快(甚至LINQ比PLINQ要快很多).

所以我們在用的時候一定要考慮到以下幾點:

  • 並不總是更快:雖然 PLINQ 可以說是可以提高某些複雜查詢的效能,但並非所有操作都會有明顯收益。執行緒管理和同步產生的開銷有時會使 PLINQ 查詢比其順序查詢慢,尤其是對於小型資料集或計算複雜度較低的操作。
  • 開銷:並行化會帶來開銷,例如任務排程和執行緒之間的切換。對非 CPU 密集型的小型集合或操作,這些開銷可能會抵消並行化的好處,從而使 PLINQ 查詢比標準 LINQ 查詢慢。
  • 排序:預設情況下,PLINQ 不保證結果的順序。如果排序很重要,則可以使用 AsOrdered 或 OrderBy 方法,但這可能會進一步降低並行化帶來的效能提升。

綜上所述,如果要用PLINQ一定要充分的進行測試與效能評估,一定要確定PLINQ有較大的提升時,才去使用.

.

相關文章