顯示具有 Asynchronous 標籤的文章。 顯示所有文章
顯示具有 Asynchronous 標籤的文章。 顯示所有文章

2023年9月25日 星期一

在 C# 內,透過 PLinq 的 AsParallel 方法做到多執行緒與平行化處理剖析

在 C# 內,透過 PLinq 的 AsParallel 方法做到多執行緒與平行化處理剖析

我最近這幾年對於 .NET 開發環境的非同步、平行處理等相關議題與需求,做了大量的研究、閱讀的相當多量的文章或者書籍,並且將這些經驗整理出相關簡報與說明範例程式碼;也有幸的能夠開設五種非同步或平行程式設計課程,透過實際與不同學員交流,將如何有效、正確地寫出好的非同步程式碼告訴更多的人。

可是,畢竟對於這方面的議題,還是存在著相當大的技術門檻,對於一般的開發者來說,這些議題還是相當的難以理解與掌握,因此,我在這裡將會透過一系列的文章,來說明如何有效地使用 .NET 開發環境內的非同步、平行處理等相關議題與需求。

首先登場的就是關於要使用 PLinq 的 AsParallel 方法,來改善原先 Linq 的查詢速度的問題。

不論是在網路的開發討論論壇、技術文章中,都會提到 PLinq & AsParallel 這兩個名詞,首先,先來了解這兩個名詞是甚麼?

  • Linq

    根據微軟的官方網頁 LINQ 查詢簡介 (C#) 的解釋 : 「查詢」是指從資料來源中擷取資料的運算式。 查詢通常以特定的查詢語言來表示。 針對各種資料來源類型開發不同的語言已有一段時間,例如用於關聯式資料庫的 SQL,以及用於 XML 的 XQuery。 因此,開發人員在過去必須針對所需支援的每種資料來源類型或資料格式,學習新的查詢語言。 LINQ 提供一致的查詢模型,以便處理各種資料來源和格式的資料,因此可簡化此情況。 在 LINQ 查詢中,您所處理的一定是物件。 您可以使用相同的基本程式碼模式來查詢和轉換 XML 檔、SQL 資料庫、ADO.NET 資料集、.NET 集合中的資料,以及 LINQ 提供者可供使用的任何其他格式。

  • PLinq

    根據微軟的官方網頁 PLINQ 簡介 的解釋 : 平行 LINQ (PLINQ) 是 Language-Integrated Query (LINQ) 模式的平行實作。 PLINQ 實作了一組完整的 LINQ 標準查詢運算子來作為 System.Linq 命名空間的擴充方法,並具有其他運算子可供平行作業使用。 PLINQ 結合了 LINQ 語法簡單易懂的特性以及平行程式設計的威力。

  • AsParallel

    啟用查詢的平行化作業

因此,若有大量查詢要處理,是否可以啟用查詢的平行化作業,就可以大幅加速整體查詢速度,又或者當有多個工作想要平行處理,將要平行處理的資料做成可列舉型態的物件,接著就可以做到平行處理的程式設計需求。

有著這樣的想法的人不在少數,然而,撞牆的開發者也佔絕大多數,最後因為無法真正使用平行化技術來解決原先問題,而自行判斷與下出結論,認為平行化技術無法解決眼前問題,然而,真的是這樣嗎?究竟是所遇到的問題真的無法使用平行程式設計方法來處理,還是當時問題的 API 無法用於平行處理,又或者是對於 I/O 或者 CPU 密集處理判斷或認知錯誤等等問題,使得無法徹底解決問題。

由於許多不正確的認知、想法、觀念、做法,著實無法一一來解釋說明,因此,只好針對不同情境,進行詳細的解剖與分析,透過有系統化的工程手法,進行驗證與測試,了解到平行處理上背後的真正運作方式,讓開發者可以實際活用這方面的技術。

在這篇文章中,將會針對一個需求,這裡會面對到有個 I/O Bound 方面的工作,也許是要做 PDF 檔案產生或者要產生 HTML 文件檔案,因為將會一次需要做到許多檔案的產生作業,立即想到可以透過平行作業方式來提升全部執行效能的提升,不用透過同步方式,一個一個的檔案來產生,造成要生成這些文件檔案需要耗時很久的時間。

因此,將需要生成檔案的資料,都先放到一個可列舉的物件內,接著透過 PLinq 提供的平行化處理功能,做到提升執行速度的結果。會使用這樣例子來說明,這是因為我有看到一個這樣的案例,因為使用了 PLinq 技術,結果還是無法提升整體執行速度,而聽到的結論是,可能使用的 API 有使用了互相鎖定機制而造成無法提升執行速度效果。

不過,在這裡將會解析隱藏在 PLinq 又或者是 .NET 中的平行程式設計 內使用到的 工作平行程式庫 (TPL Task Parallel Library) 技術後面的原理。一旦,理解與明瞭這樣的設計與運作方式,日後就可以很容易地解決相關問題,而不會找出許多文不對題的解釋理由。

建立 PLinq 的 AsParallel 方法 測試專案

為了簡化測試用專案的複雜度,因此,在這裡將會建立一個 Console 主控台應用類型的專案。

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 csAsParallel 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 7.0 (標準字詞支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

撰寫測試用的程式碼

  • 在此專案節點下,找到並且打開 [Program.cs] 這個檔案
  • 使用底下 C# 程式碼替換掉 [Program.cs] 檔案內所有程式碼內容
namespace csAsParallel
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var source = Enumerable.Range(1, 10000);

            var evenNums = source.AsParallel()
                .Select(ShowInfo);

            Console.WriteLine($"Total {evenNums.Count()}");
        }

        static int ShowInfo(int n)
        {
            Console.WriteLine($"N={n:d6} {DateTime.Now:mm:ss} - {n} / Thread ID {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(5000);
            return n;
        }
    }  
}

在這裡將會使用 [Enumerable.Range(1, 10000)] 方法,產生出 10 萬個整數數值列舉物件, source ,接著對這個列著列舉物件下達 AsParallel() 方法呼叫,讓這個 Linq 敘述,轉換成為要使用 PLinq 平行處理的計算方式。

最後,使用了 Select(ShowInfo) 方法,產生出新的列舉值,不過,這裡所生成的列舉值,將會使用平行處理方式來處理。

對於 [ShowInfo] 這個方法,將會使用 Thread.Sleep(5000) 方法,模擬要產生 PDF 或者 HTML 檔案所要花費的時間,而在這個方法之前,將會列出所接收到的列舉項目的數值(整數值),當時的時間,當前所用的執行緒 ID。

整體看來,這個範例程式似乎相當的簡單與容易了解,解設當產生的列舉值總共有 100 個,身為開發者的你,可以預期、預估這個程式碼需要耗費多少的時間才能夠執行完成執行完成呢?

是要 100 * 5 (秒) = 500 (秒) , 還是全部執行完成僅需要 5 秒呢?身為讀者的你會認為需要多久的時間呢?若真的全部完成需要 5 秒,真的做得到嗎?

現在開始來解開隱藏在背後的祕密之一

在 8 個邏輯處理器下的運行結果

將上述的專案採用底下的模式來發布

  • 採用發佈到資料夾模式
  • 部署模式設定為 獨立式
  • 目標執行階段設定為 win-x64
  • 檔案發行選項內要勾選 產生單一檔案 與 修剪未使用的程式碼
  • 底下為發布後所產生的檔案

現在準備了一台具有 8 個邏輯處器的主機,將發布後的兩個檔案複製到這台主機上

透過工作管理員視窗內 [效能] > [CPU] 看到相關 CPU 效能數據,其中在右下角區域,將會看到虛擬邏輯處理器數量確定為 8

透過命令提示字元視窗來執行這個 csAsParallel.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csAsParallel.exe
N=000001 20:48 - 1 / Thread ID 1
N=000008 20:48 - 8 / Thread ID 11
N=000006 20:48 - 6 / Thread ID 9
N=000005 20:48 - 5 / Thread ID 8
N=000007 20:48 - 7 / Thread ID 10
N=000002 20:48 - 2 / Thread ID 4
N=000004 20:48 - 4 / Thread ID 7
N=000003 20:48 - 3 / Thread ID 6
N=000009 20:53 - 9 / Thread ID 4
N=000012 20:53 - 12 / Thread ID 8
N=000010 20:53 - 10 / Thread ID 6
N=000013 20:53 - 13 / Thread ID 1
N=000011 20:53 - 11 / Thread ID 7
N=000014 20:53 - 14 / Thread ID 10
N=000015 20:53 - 15 / Thread ID 9
N=000016 20:53 - 16 / Thread ID 11
N=000017 20:58 - 17 / Thread ID 11
N=000024 20:58 - 24 / Thread ID 8
N=000019 20:58 - 19 / Thread ID 9
N=000020 20:58 - 20 / Thread ID 1
N=000021 20:58 - 21 / Thread ID 4
N=000022 20:58 - 22 / Thread ID 10
N=000023 20:58 - 23 / Thread ID 7
N=000018 20:58 - 18 / Thread ID 6
^C
C:\Vulcan\win-x64>

從上面的執行結果文字,可以看到

  • 這台具有 8 個邏輯處理器電腦,每 5 秒鐘,同時執行 8 八個執行緒
  • 當 5 秒鐘過後,又會有另外 8 個執行緒跑起來,每個執行緒需要 5 秒執行時間
  • 從 N 這個後面的值看到,這個 PLinq 敘述其實把整個列舉數值,切割成為 8 個區塊 Chunk ,每個 Chunk 內的整數列舉,將會依序交給一個執行緒來執行
  • 每次需要執行緒來執行程式碼的時候,將會透過執行緒集區來取得一個執行緒來執行委派方法

結論:因為這台電腦具有 8 個邏輯處理器,因為, PLinq 會將整個列舉整數切割成為 8 個區塊,並且透過 8 個執行緒來處理每個區塊內的整數數值,因此,整體最終完成時間將會是 100000 / 8 * 5 = xxx 秒。

在 4 個邏輯處理器下的運行結果

現在準備了一台具有 4 個邏輯處器的主機,將發布後的兩個檔案複製到這台主機上

透過工作管理員視窗內 [效能] > [CPU] 看到相關 CPU 效能數據,其中在右下角區域,將會看到虛擬邏輯處理器數量確定為 4

透過命令提示字元視窗來執行這個 csAsParallel.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csAsParallel.exe
N=000003 18:45 - 3 / Thread ID 4
N=000001 18:45 - 1 / Thread ID 1
N=000002 18:45 - 2 / Thread ID 7
N=000004 18:45 - 4 / Thread ID 6
N=000006 18:50 - 6 / Thread ID 4
N=000007 18:50 - 7 / Thread ID 1
N=000005 18:50 - 5 / Thread ID 7
N=000008 18:50 - 8 / Thread ID 6
N=000012 18:55 - 12 / Thread ID 7
N=000011 18:55 - 11 / Thread ID 1
N=000009 18:55 - 9 / Thread ID 6
N=000010 18:55 - 10 / Thread ID 4
N=000016 19:00 - 16 / Thread ID 7
N=000013 19:00 - 13 / Thread ID 1
N=000015 19:00 - 15 / Thread ID 4
N=000014 19:00 - 14 / Thread ID 6
N=000018 19:05 - 18 / Thread ID 4
N=000019 19:05 - 19 / Thread ID 1
N=000017 19:05 - 17 / Thread ID 6
N=000020 19:05 - 20 / Thread ID 7
^C
C:\Vulcan\win-x64>

從上面的執行結果文字,可以看到

  • 這台具有 4 個邏輯處理器電腦,每 5 秒鐘,同時執行 4 八個執行緒
  • 當 5 秒鐘過後,又會有另外 4 個執行緒跑起來,每個執行緒需要 5 秒執行時間
  • 從 N 這個後面的值看到,這個 PLinq 敘述其實把整個列舉數值,切割成為 4 個區塊 Chunk ,每個 Chunk 內的整數列舉,將會依序交給一個執行緒來執行
  • 每次需要執行緒來執行程式碼的時候,將會透過執行緒集區來取得一個執行緒來執行委派方法

結論:因為這台電腦具有 4 個邏輯處理器,因為, PLinq 會將整個列舉整數切割成為 4 個區塊,並且透過 4 個執行緒來處理每個區塊內的整數數值,因此,整體最終完成時間將會是 100000 / 8 * 5 = xxx 秒。

在 2 個邏輯處理器下的運行結果

現在準備了一台具有 2 個邏輯處器的主機,將發布後的兩個檔案複製到這台主機上

透過工作管理員視窗內 [效能] > [CPU] 看到相關 CPU 效能數據,其中在右下角區域,將會看到虛擬邏輯處理器數量確定為 2

透過命令提示字元視窗來執行這個 csAsParallel.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csAsParallel.exe
N=000001 33:43 - 1 / Thread ID 1
N=000002 33:43 - 2 / Thread ID 4
N=000003 33:48 - 3 / Thread ID 1
N=000004 33:48 - 4 / Thread ID 4
N=000005 33:53 - 5 / Thread ID 1
N=000006 33:53 - 6 / Thread ID 4
N=000007 33:58 - 7 / Thread ID 1
N=000008 33:58 - 8 / Thread ID 4
N=000009 34:03 - 9 / Thread ID 1
N=000010 34:03 - 10 / Thread ID 4
N=000011 34:08 - 11 / Thread ID 1
N=000012 34:08 - 12 / Thread ID 4
^C
C:\Vulcan\win-x64>

從上面的執行結果文字,可以看到

  • 這台具有 2 個邏輯處理器電腦,每 5 秒鐘,同時執行 2 八個執行緒
  • 當 5 秒鐘過後,又會有另外 2 個執行緒跑起來,每個執行緒需要 5 秒執行時間
  • 從 N 這個後面的值看到,這個 PLinq 敘述其實把整個列舉數值,切割成為 2 個區塊 Chunk ,每個 Chunk 內的整數列舉,將會依序交給一個執行緒來執行
  • 每次需要執行緒來執行程式碼的時候,將會透過執行緒集區來取得一個執行緒來執行委派方法

結論:因為這台電腦具有 2 個邏輯處理器,因為, PLinq 會將整個列舉整數切割成為 2 個區塊,並且透過 2 個執行緒來處理每個區塊內的整數數值,因此,整體最終完成時間將會是 100000 / 2 * 5 = xxx 秒。

在 1 個邏輯處理器下的運行結果

現在準備了一台具有 1 個邏輯處器的主機,將發布後的兩個檔案複製到這台主機上

透過工作管理員視窗內 [效能] > [CPU] 看到相關 CPU 效能數據,其中在右下角區域,將會看到虛擬邏輯處理器數量確定為 1

透過命令提示字元視窗來執行這個 csAsParallel.exe 程式,將會看到底下輸出結果

C:\Vulcan\win-x64>csAsParallel.exe
N=000001 39:05 - 1 / Thread ID 1
N=000002 39:10 - 2 / Thread ID 1
N=000003 39:15 - 3 / Thread ID 1
N=000004 39:20 - 4 / Thread ID 1
N=000005 39:25 - 5 / Thread ID 1
N=000006 39:30 - 6 / Thread ID 1
N=000007 39:35 - 7 / Thread ID 1
^C
C:\Vulcan\win-x64>

從上面的執行結果文字,可以看到

  • 這台具有 1 個邏輯處理器電腦,每 5 秒鐘,同時執行 1 八個執行緒
  • 當 5 秒鐘過後,又會有另外 1 個執行緒跑起來,每個執行緒需要 5 秒執行時間
  • 從 N 這個後面的值看到,這個 PLinq 敘述其實把整個列舉數值,切割成為 1 個區塊 Chunk ,每個 Chunk 內的整數列舉,將會依序交給一個執行緒來執行
  • 每次需要執行緒來執行程式碼的時候,將會透過執行緒集區來取得一個執行緒來執行委派方法

結論:因為這台電腦具有 1 個邏輯處理器,因為, PLinq 會將整個列舉整數切割成為 1 個區塊,並且透過 1 個執行緒來處理每個區塊內的整數數值,因此,整體最終完成時間將會是 100000 / 1 * 5 = xxx 秒。 






2022年9月2日 星期五

如何設計有效的 Parallel.ForEachAsync 迴圈 - 需要使用平行計算方式來執行 async Method 非同步方法

如何設計有效的 Parallel.ForEachAsync 迴圈 - 需要使用平行計算方式來執行 async Method 非同步方法

對於要使用 [Parallel.ForEach] 來設計出一個具有平行作業的應用程式,相信對於許多 .NET C# 開發者而言,應該不是個很大的問題,此時,可以參考 如何:撰寫簡單的 Parallel.ForEach 迴圈 這篇文章中的說明與範例程式碼,相信可以很容易得輕鬆上手。

可是對於 Parallel.ForEach 方法 的使用,需要傳入至少一個列舉 IEnumerable 與一個委派方法,不過,在這裡若是設計使用同步方法來進行設計程式碼,相信執行上一切都沒有問題, Parallel.ForEach 方法將會使用平行計算的方式,使用列舉物件內的值,平行執行與傳入到這個委派方法,但是,當這個委派方法已經改成一個非同步方法,也就是在這個委派方法有加入了 async 這個修飾詞,那麼將會發現到,一旦執行到 Parallel.ForEach 之後,將會馬上就執行到下一行敘述,不會等到所有的列舉物件都執行完成後,才會繼續往下執行

對於使用同步委派方法來設計 Parallel.ForEach 的應用,可以參考底下的範例程式碼

#region 若在 Parallel.Foreach 內使用同步方式進行委派方法的設計,會等到所有委派方法都執行完畢後,才會繼續往下執行
Console.WriteLine($"使用 Parallel.Foreach 與同步委派方法 開始 {DateTime.Now}");
Parallel.ForEach(Enumerable.Range(0, 3), async (x, t) =>
{
    Console.WriteLine("  Bpfsync");
    Thread.Sleep(3000);
    Console.WriteLine("  Cpfsync");
});
// 當看到這行敘述,表示 Parallel.ForEach 已經結束執行,不過,將還沒看到所有的 Cpf 文字輸出
Console.WriteLine($"使用 Parallel.Foreach 與同步委派方法 結束 {DateTime.Now}");

在這裡,將會使用 Enumerable.Range(0, 3) 一個列舉物件,且這個列舉物件內有三個值,並使用 Lambda 指定一個委派方法,該方法將會使用 Thread.Sleep(3000) 模擬該方法需要花費三秒鐘的時間來執行某些工作,整個 Lambda 委派方法將會是使用同步方式來進行運行。

從底下的執行結果可以看出,一旦 Parallel.ForEach 開始執行之後,將會平行執行 Enumerable.Range(0, 3) 列舉物件內的每個值,

使用 Parallel.Foreach 與同步委派方法 開始 2000/9/2 上午 09:23:09
  Bpfsync
  Bpfsync
  Bpfsync
  Cpfsync
  Cpfsync
  Cpfsync
使用 Parallel.Foreach 與同步委派方法 結束 2000/9/2 上午 09:23:12

若將程式碼改成如下,將每個平行執行的傳入物件值顯示出來

#region 若在 Parallel.Foreach 內使用同步方式進行委派方法的設計,會等到所有委派方法都執行完畢後,才會繼續往下執行
Console.WriteLine($"使用 Parallel.Foreach 與同步委派方法 開始 {DateTime.Now}");
Parallel.ForEach(Enumerable.Range(0, 3), async (x, t) =>
{
    Console.WriteLine($"  Bpfsync {x}");
    Thread.Sleep(3000);
    Console.WriteLine($"  Cpfsync {x}");
});
// 當看到這行敘述,表示 Parallel.ForEach 已經結束執行,不過,將還沒看到所有的 Cpf 文字輸出
Console.WriteLine($"使用 Parallel.Foreach 與同步委派方法 結束 {DateTime.Now}");
#endregion

這裡將會是上面程式碼執行後的結果,從這裡可以再度驗證與得到一個結論, Parallel.ForEach 確實逐一平行來執行美個委派方法,可以在平行執行作業過程中,是每有說哪個執行緒必須一定要先執行,或者要按著當初啟動的順序來逐一平行執行的慣例或者說法。

使用 Parallel.Foreach 與同步委派方法 開始 2000/9/2 上午 09:34:12
  Bpfsync 0
  Bpfsync 1
  Bpfsync 2
  Cpfsync 0
  Cpfsync 2
  Cpfsync 1
使用 Parallel.Foreach 與同步委派方法 結束 2000/9/2 上午 09:34:15

現在將要平行運行的委派方法改成非同步委派方法,因此,原先使用 Thread.Sleep(3000) 這個敘述,將會改成使用 await Task.Delay(3000) 這樣的敘述,然而,因為使用了 await 運算子,所以,就需要在委派方法前面加上 async 修飾詞,底下將會是這樣的程式碼

#region 若在 Parallel.Foreach 內使用 async 方法,將會立即結束平行敘述,相關程式碼會在背景執行中
Console.WriteLine($"使用 Parallel.Foreach 開始 {DateTime.Now}");
Parallel.ForEach(Enumerable.Range(0, 3), async (x, t) =>
{
    Console.WriteLine($"  Bpf {x}");
    await Task.Delay(3000);
    Console.WriteLine($"  Cpf {x}");
});
// 當看到這行敘述,表示 Parallel.ForEach 已經結束執行,不過,將還沒看到所有的 Cpf 文字輸出
Console.WriteLine($"使用 Parallel.Foreach 結束 {DateTime.Now}");
#endregion


#region 故意休息五秒,等待上述的平行作業全部都結束
Console.WriteLine();
Console.WriteLine($"休息 五秒鐘");
await Task.Delay(5000);
Console.WriteLine();

因為若在 Parallel.Foreach 內使用 async 方法,將會立即結束平行敘述,相關委派方法內程式碼會仍在背景執行中,因此,無法透過 Parallel.ForEach 來得知是否所有的平行運算都已經全部完成了;由於平行計算模擬花費 3 秒計算時間,而這三秒將會在背景下運行,因此,這裡使用 await Task.Delay(5000) 這樣的敘述,故意休息五秒,等待上述的所有背景平行作業全部都結束,因此將會看到底下的輸出結果;從執行結果可以看出,當 [休息 五秒鐘] 文字顯示之後,並且真的讓當前執行緒強制睡眠五秒鐘之後,約在三秒之後,就會看到每個委派方法執行結束的文字輸出。

使用 Parallel.Foreach 開始 2022/9/2 上午 09:34:15
  Bpf 0
  Bpf 2
  Bpf 1
使用 Parallel.Foreach 結束 2022/9/2 上午 09:34:15

休息 五秒鐘
  Cpf 2
  Cpf 0
  Cpf 1

為了解決這樣的應用,在 .NET 6 的 BCL 中,將會提供了 Parallel.ForEachAsync 方法 ,透過這個方法,將會可以做到使用 Parallel 類別提供的功能,並且使用非同步的方法來平行執行委派方法

#region 這裡使用 Parallel.ForEachAsync 來平行非同步方法,將不會有上述問題,全部的非同步作業都平行執行完畢,該行敘述才會繼續往下執行,這可以從時間戳記看出
Console.WriteLine($"使用 Parallel.ForEachAsync 開始 {DateTime.Now}");
await Parallel.ForEachAsync(Enumerable.Range(0, 3), async (x, t) =>
{
    Console.WriteLine($"  Bpfa {x}");
    await Task.Delay(3000);
    Console.WriteLine($"  Cpfa {x}");
});
Console.WriteLine($"使用 Parallel.ForEachAsync 結束 {DateTime.Now}");
#endregion

在這裡將會使用 await Parallel.ForEachAsync 來使用非封鎖方式來等待所有的平行作業都執行完畢,而且這些要採用平行執行的委派方法,都將採用非同步方法(有 async 修飾詞的方法)來設計。

從底下的執行結果,應該是當初需求所期望能夠設計出來的功能

使用 Parallel.ForEachAsync 開始 2000/9/2 上午 09:34:20
  Bpfa 0
  Bpfa 1
  Bpfa 2
  Cpfa 2
  Cpfa 1
  Cpfa 0 

使用 Parallel.ForEachAsync 結束 2000/9/2 上午 09:34:27