2022年9月22日 星期四

在 .NET C# 下使用 MQTT 來訂閱與發佈訊息

在 .NET C# 下使用 MQTT 來訂閱與發佈訊息

對於 MQTT 的定義,在維基百科上是這麼說的:訊息佇列遙測傳輸(英語: Message Queuing Telemetry Transport,MQTT )是ISO 標準(ISO/IEC PRF 20922)下基於發布 (Publish)/訂閱 (Subscribe)範式的訊息協定,可視為「資料傳遞的橋梁」它工作在 TCP/IP協定族上,是為硬體效能低下的遠端裝置以及網路狀況糟糕的情況下而設計的發布/訂閱型訊息協定,為此,它需要一個訊息中介軟體,以解決當前繁重的資料傳輸協定,如:HTTP。

首先, MQTT 是 Message Queuing Telemetry Transport 的縮寫,從字面意義來看,就是把要傳遞的訊息放在一個佇列內,接著可以傳送 Publish 到別的地方,而需要接收到這個訊息的用戶,則是需要訂閱這主題類型的訊息,便可以收到來自遠端傳送過來的訊息,所以,上面解釋文章有提到:可視為「資料傳遞的橋梁」,也就是這樣的意思。

在這裡將會使用 MQTTNet 這個套件來進行操作,看看如何在分散式的環境中,進行資料傳遞操作。

建立測試用 MQTT 伺服器端 的主控台應用程式專案

  • 打開 Visual Studio 2022
  • 點選右下方的 [建立新的專案] 按鈕
  • 選擇一個 [主控台應用程式] 的專案範本
  • 點選右下方的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗內,在 [專案名稱] 欄位中,輸入 csMqttServer
  • 點選右下方的 [下一步] 按鈕
  • 在 [其他資訊] 對話窗中
  • 取消 [Do not use top-level statements] 這個 checkbox 檢查盒的勾選
  • 點選右下方的 [建立] 按鈕
  • 透過 NuGet 工具,搜尋到 MQTTnet 這個套件,將其安裝到這個專案
  • 打開 Program.cs 檔案,將底下內容替換掉這個檔案內容
using MQTTnet.Server;
using MQTTnet;
using System.Text;

namespace csMqttServer;

internal class Program
{
    private static IMqttServer _mqttServer;

    static void Main(string[] args)
    {

        // 設定 MQTT server 的運作環境參數
        var optionsBuilder = new MqttServerOptionsBuilder()
            .WithConnectionBacklog(100)
            .WithDefaultEndpointPort(1884)
            .WithMaxPendingMessagesPerClient(1000);

        // 透過 MqttFactory 這個工廠方法類別,建立一個 MQTT 伺服器物件
        _mqttServer = new MqttFactory().CreateMqttServer();

        // 綁定當訊息送到此伺服器之後的事件,定義該做甚麼事情
        _mqttServer.UseApplicationMessageReceivedHandler(async e =>
        {
            // 對於此訊息的 Payload 部分,將會是經過編碼的,因此需要先解碼
            var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
            var topic = e.ApplicationMessage.Topic;
            if (topic == "Hi")
            {
                Console.WriteLine("subscription message received");
                Console.WriteLine("Simulating Say Hello messages...");
                await SimulateSayHelloAsync();
            }
        });

        // 一旦有用戶端連線上來之後,將會觸發這個事件
        _mqttServer.UseClientConnectedHandler(e =>
        {
            Console.WriteLine("***** CLIENT CONNECTED : " + e.ClientId + " *******");
        });

        // 啟動這個 MQTT 伺服器
        _mqttServer.StartAsync(optionsBuilder.Build());

        Console.ReadLine();
    }

    private static async Task SimulateSayHelloAsync()
    {
        await PublishMessage("HiEcho", "Hello Word!");
    }

    private static async Task PublishMessage(string topic, string message)
    {
        // 產生一個 MQTT 訊息
        var mqttMessage = new MqttApplicationMessageBuilder()
                            .WithTopic(topic)
                            .WithPayload(message)
                            .WithAtLeastOnceQoS()
                            .WithRetainFlag(false)
                            .WithDupFlag(false)
                            .Build();

        // 發佈此剛剛建立的訊息
        var result = await _mqttServer.PublishAsync(mqttMessage, CancellationToken.None);

        if (result.ReasonCode == MQTTnet.Client.Publishing.MqttClientPublishReasonCode.Success)
            Console.WriteLine("Message published : " + message);
    }
}

這是一個 MQTT 伺服器端的應用程式,這個程式最後將會 Console.ReadLine(); 敘述,除非使用者在命令提示字元視窗內按下 Enter 按鍵,否則這個 MQTT 伺服器會一直持續執行中。

首先建立一個 MqttServerOptionsBuilder 物件,這是要用來設定 MQTT server 的運作環境參數,接著使用 new MqttFactory().CreateMqttServer() 來建立一個 MQTT 伺服器的物件。

一旦取得這個伺服器物件之後,緊接著便可以進行一些事件綁定的程式碼設計,首先先使用 UseApplicationMessageReceivedHandler 這個事件綁定到一個委派方法內,用來定義當訊息送到此伺服器之後的事件,定義該做甚麼事情。

在這個練習中,將會判斷當伺服器收到的訊息之主題為 Hi 這個名稱,便會在這個方法內呼叫 await SimulateSayHelloAsync(); 敘述,對送出此訊息的用戶端,送出回應訊息。

綁定 MQTT 伺服器上的第二個事件為 UseClientConnectedHandler ,當有用戶端成功連線到這台伺服器上之後,便會出發這個事件,在此將會僅僅顯示一個訊息到螢幕上,當然,在實際用途上,可以做出其他層面的不同應用設計。

最後要來看看 PublishMessage 這個方法,如何在伺服器上,對於用戶端發送訊息,這個方法將會被 SimulateSayHelloAsync 所呼叫,而此方法將會傳送 Topic 主題與 Message 訊息內容物件過來,首先會先透過建立 MqttApplicationMessageBuilder 物件,指定要傳送訊息的主題、內容、與其他相關傳遞參數,接著呼叫 MQTT 伺服器物件上的 PublishAsync 方法,將剛剛建立好的 MqttApplicationMessage 物件,送出到有訂閱該主題的用戶端上。

啟動與執行該 MQTT 伺服器專案

若沒有問題,可以執行上面寫的專案,讓這個 MQTT 伺服器可以運作起來。

建立測試用 MQTT 用戶端 的主控台應用程式專案

  • 打開 Visual Studio 2022
  • 點選右下方的 [建立新的專案] 按鈕
  • 選擇一個 [主控台應用程式] 的專案範本
  • 點選右下方的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗內,在 [專案名稱] 欄位中,輸入 csMqttClient
  • 點選右下方的 [下一步] 按鈕
  • 在 [其他資訊] 對話窗中
  • 取消 [Do not use top-level statements] 這個 checkbox 檢查盒的勾選
  • 點選右下方的 [建立] 按鈕
  • 透過 NuGet 工具,搜尋到 MQTTnet 這個套件,將其安裝到這個專案
  • 打開 Program.cs 檔案,將底下內容替換掉這個檔案內容
using MQTTnet.Client.Options;
using MQTTnet.Client;
using MQTTnet;
using System.Text;
using MQTTnet.Client.Subscribing;

namespace csMqttClient;

internal class Program
{
    private static IMqttClient _mqttClient;

    static void Main(string[] args)
    {
        // 使用 MqttFactory 工廠類別,建立一個 MQTT 用戶端的物件
        _mqttClient = new MqttFactory().CreateMqttClient();

        // 每個用戶端都要有唯一的 Id,並且要再宣告 MQTT 伺服器的連線位址與方式
        var options = new MqttClientOptionsBuilder().WithClientId("MqttClient")
                                                    .WithTcpServer("localhost", 1884)
                                                    .Build();

        // 綁定當此用戶端已經連線到遠端伺服器之後,所要處理的工作
        _mqttClient.UseConnectedHandler(async e =>
        {
            // 在此訂閱有興趣的主題訊息
            MqttClientSubscribeResult subResult =
            await _mqttClient.SubscribeAsync(new MqttClientSubscribeOptionsBuilder()
            .WithTopicFilter("HiEcho")
            .Build());

            // 對伺服器送出問好訊息
            PublishMessage("Hi", "Test Message");
        });

        // 綁定當訊息傳送到此用戶端之後,將會觸發的事件
        _mqttClient.UseApplicationMessageReceivedHandler(e =>
        {
            Console.WriteLine($"來自伺服器的問安 {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}");
        });

        // 連線到遠端伺服器上
        _mqttClient.ConnectAsync(options, CancellationToken.None);

        Console.Read();
    }

    private static async void PublishMessage(string topic, string message)
    {
        // Create mqttMessage
        var mqttMessage = new MqttApplicationMessageBuilder()
                            .WithTopic(topic)
                            .WithPayload(message)
                            .WithExactlyOnceQoS()
                            .Build();

        // Publish the message asynchronously
        await _mqttClient.PublishAsync(mqttMessage, CancellationToken.None);
    }
}

現在要來設計 MQTT 用戶端的應用程式,這裡使用 new MqttFactory().CreateMqttClient(); 敘述建立一個 _mqttClient 物件,接著透過 new MqttClientOptionsBuilder().WithClientId("MqttClient").WithTcpServer("localhost", 1884).Build(); 敘述指定該 MQTT 用戶端的 ID 與要連線到後端 MQTT 伺服器上的連線資訊,如此,將會得到一個 IMqttClientOptions 型別的物件。

現在要透過呼叫 PublishMessage 自訂方法來將這個 MQTT 訊息傳送出去。

建立一個 MqttApplicationMessageBuilder 物件,指定要送出訊息內容與該訊息搭配的 Topic,便可以使用 await _mqttClient.PublishAsync(mqttMessage, CancellationToken.None); 敘述將這個訊息送到遠端伺服器上

啟動與執行該 MQTT 用戶端專案

若沒有問題,可以執行上面寫的專案,讓這個 MQTT 伺服器可以運作起來。

執行結果

從底下兩個主控台螢幕視窗輸入內容,可以看到這兩個專案的執行結果

這是 MQTT 伺服器執行的輸出內容

這是 MQTT 用戶端執行的輸出內容 




2022年9月6日 星期二

在 WPF 上建立與使用 Lottie 來設計出具有動畫效果

在 WPF 上建立與使用 Lottie 來設計出具有動畫效果

之前有寫過一篇 建立與使用 Lottie 來設計出具有動畫效果的 App 文章,說明如何在 .NET MAUI 專案中,使用 Lottie 檔案,產生出具有動畫效果的應用程式,這兩天聽到有個 WPF 上有這樣類似的需求,就想說是否可以在 WPF 上也能夠做出這樣的效果呢?

抱持這這樣的疑問,便開始先在網路進行搜尋,想要在 WPF 下來使用 Lottie 這樣機制的時候,究竟要使用哪個套件比較好,姊果發現到,現在可以找到的 Lottie 套件僅能夠在 .NET 5 以上的環境上運作。

而甚麼事 Lottie 呢?這是在 2015 年之後才出現的一個功能,根據維基百科上的描述可以得到這方面的說明

Lottie is a file format for vectorial animation, and is named after Charlotte Reiniger, a German pioneer of silhouette animation.

在這個 https://lottiefiles.com/ 網站上,可以看到種 Lottie 做出的動畫效果,而且可以下載到相當多的動畫 JSON 檔案

現在將來嘗試在 WPF 專案下,透過隨便下載的 Lottie JSON 檔案,將這個動畫能夠在 WPF 應用程式下來運行

建立 WPF 專案

  • 開啟 Visual Studio 2022 開發工具

  • 當 [Visual Studio 2022] 對話窗出現的時候

  • 點選右下角的 [建立新的專案] 按鈕選項

  • 現在將看到 [建立新專案] 對話窗

  • 切換右上角的 [所有專案類型] 下拉選單控制項

  • 找到並且點選 [桌面] 這個選項

  • 從清單中找到並選擇 [WPF 應用程式] 這個專案範本 (記得要選擇使用 C# 程式語言)

    此專案可用於建立 .NET WPF 應用程式

  • 點選右下角的 [下一步] 按鈕

  • 此時將會看到 [設定新的專案] 對話窗

  • 在 [專案名稱] 欄位,輸入 wpfLottie

  • 點選右下角的 [下一步] 按鈕

  • 最後會看到 [其他資訊] 對話窗

  • 使用預設設定值,也就是 [架構] 為 [.NET 6.0 (長期支援)]

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

加入 LottieSharp 的 NuGet 套件

  • 滑鼠右擊該專案的 [相依性] 節點
  • 從彈出功能表中選擇 [管理 NuGet 套件] 功能選項
  • 此時,[NuGet: wpfLottie] 視窗將會出現
  • 點選 [瀏覽] 標籤頁次
  • 在左上方的搜尋文字輸入盒內輸入 LottieSharp 關鍵字
  • 若你沒有看到 8.0 以上的版本,請勾選 [包括搶鮮版] 檢查盒控制項
  • 現在,將會看到 LottieSharp 套件出現在清單內
  • 點選這個 LottieSharp 套件,並且點選右上方的 [安裝] 按鈕,安裝這個套件到這個專案內。

下載與複製 Lottie 檔案道專案內

  • 下載完成後的檔案名稱將會為 [9945-space-launch.json]
  • 透過檔案總管拖拉這個檔案到剛剛建立的 WPF 專案根目錄下
  • 在方案總管點選這個檔案
  • 在 [屬性] 視窗內
  • 設定 [建置動作] 的屬性值為 [永遠複製]

設計使用 Lottie 元件的視窗

  • 打開 [MainWindow.xaml] 檔案
  • 將底下 XAML 宣告標記覆蓋掉這個檔案的內容
<Window x:Class="wpfLottie.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:lottie="clr-namespace:LottieSharp.WPF;assembly=LottieSharp"
       xmlns:local="clr-namespace:wpfLottie"
        mc:Ignorable="d"
        Title="Lottie 在 WPF 上的使用範例" Height="450" Width="800">
    <Grid>
        <lottie:LottieAnimationView
    Width="200"
    Height="300"
    HorizontalAlignment="Center"
    VerticalAlignment="Center"
    AutoPlay="True"
    FileName="9945-space-launch.json"
    RepeatCount="-1" />
    </Grid>
</Window>

啟動與執行

  • 底下會是執行後的運作畫面

 




2022年9月3日 星期六

.NET MAUI 可以使用雙指放大與移動的設計效果

.NET MAUI 可以使用雙指放大與移動的設計效果

建立 .NET MAUI 應用程式 專案

  • 開啟 Visual Studio 2022 版本

  • 點選螢幕右下角的 [建立新的專案] 按鈕

  • 切換右上角的 [所有專案類型] 下拉選單控制項

  • 找到並且點選 [MAUI] 這個選項

  • 從清單中找到並選擇 [.NET MAUI 應用程式] 這個專案範本

    此專案可用於建立適用於 iOS、Android、Mac Catalyst、Tizen和WinUI 的 .NET MAUI 應用程式

  • 點選右下角的 [下一步] 按鈕

  • 當出現了 [設定新的專案] 對話窗

  • 在 [專案名稱] 欄位內,輸入 mauiPinchZoomImage

  • 點選右下角的 [下一步] 按鈕

  • 當出現了 [其他資訊] 對話窗

  • 對於 [架構] 的下拉選單控制項,使用預設值

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

加入 PropertyChanged.Fody 的 NuGet 套件

  • 滑鼠右擊該專案的 [相依性] 節點
  • 從彈出功能表中選擇 [管理 NuGet 套件] 功能選項
  • 此時,[NuGet: mauiLottie] 視窗將會出現
  • 點選 [瀏覽] 標籤頁次
  • 在左上方的搜尋文字輸入盒內輸入 Bertuzzi.MAUI.PinchZoomImage 關鍵字
  • 現在,將會看到 Bertuzzi.MAUI.PinchZoomImage 套件出現在清單內
  • 點選這個 Bertuzzi.MAUI.PinchZoomImage 套件,並且點選右上方的 [安裝] 按鈕,安裝這個套件到這個專案內。

修正頁面

  • 在根目錄下找到並且打開 MainPage.xaml 這個檔案

  • 在這個 XAML 檔案內的 ContentPage 根節點內,加入一個新的命名空間

    xmlns:pinch="clr-namespace:Bertuzzi.MAUI.PinchZoomImage;assembly=Bertuzzi.MAUI.PinchZoomImage"

  • 將這個頁面的整個 XAML 內容,使用底下的 XAML 標記來替換

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:pinch="clr-namespace:Bertuzzi.MAUI.PinchZoomImage;assembly=Bertuzzi.MAUI.PinchZoomImage"
             x:Class="mauiPinchZoomImage.MainPage">

    <StackLayout HorizontalOptions="Center" VerticalOptions="Center"
            Spacing="25"
            Padding="30,0">

        <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />

        <pinch:PinchZoom
            Margin="20">
            <Image Source="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Vervet_Monkey_%28Chlorocebus_pygerythrus%29.jpg/1200px-Vervet_Monkey_%28Chlorocebus_pygerythrus%29.jpg?20181109194254"/>
        </pinch:PinchZoom>
    </StackLayout>

</ContentPage>

在這個頁面內,使用 StackLayout 作為這個頁面的根項目,在最後面使用 pinch:PinchZoon 來加入這個可以縮放的控制項,而在這個控制項內容屬性 Content Property 指定一個 Image 做為其要顯示的內容,這裡將會隨機從網路找到一個圖片 URL ,做為要顯示這個圖片的來源。

現在可以建置與執行看看,將會得到底下的畫面

現在可以使用雙指在猴子圖片上,進行捏合與放開的操作,就可以看到圖片正在即時的放大效果,而,若使用單指在猴子圖片上進行拖拉,則可以移動這個圖片的位置。

 





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