只有累積,沒有奇蹟

2024年1月15日 星期一

[NET] 再探 Task.WaitAll 與 Task.WhenAll 差異

前言
之前自己對於 Task.WaitAll 與 WhenAll 有些一知半解的地方,因此進行研究後並撰寫了 [.NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll 文章,最近在與公司同事 Code Review 再度討論起兩者的使用方式,主管也不吝的與團隊成員分享對於 WaitAll 與 WhenAll 主要的看法與使用上的差異,這篇筆記簡單記錄當時討論的內容與結論,內容若有問題歡迎提出來一起討論。

差異性
首先,在之前文章[.NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll 中思考的出發點是以效率出發,而建議使用 Task.WhenAll 取代 Task.WaitAll 方法,但可能在使用上還是有其他需要考慮的部分,其實效率並不是兩個方法的主要差異,差異性有以下兩點

例外處理 Exception
Task.WaitAll() 處理所有返回 Task 的 Exception
Task.WhenAll() 只能處理第一個返回 Task 的 Exception
我們接下來可以透過簡單的 Code 來說明例外處理的部分,下列代碼定義了兩個 Function 分別是 throw IndexOutOfRangeException 與 NullReferenceException,在 main 的方法使用 WaitAll 並使用 try catch 將觀察例外處理 exception 部分為何
class Program
{
    static async Task Main(string[] args)
    {
        try 
        {
            Task.WaitAll(ThrowIndexOfRangeException(), ThrowNullReferenceException());
        }
        catch (Exception ex) 
        {
            Console.WriteLine(ex);
        }
    }
    
    
    static private Task ThrowIndexOfRangeException()
    {
        return Task.Run(() => { throw new IndexOutOfRangeException();});
    }

    static private Task ThrowNullReferenceException()
    {
        return Task.Run(() => { throw new NullReferenceException(); });
    }
}
開啟 Rider在 catch 下中斷點,可以從 Console 可以看到有抓到兩個 exception,拋出的例外 excption 都有抓到看起來合情合理
接著將 WhenAll 來替換定原有的 WaitAll 方法,再重新跑一次觀察例外處理 exception 是否相同
static async Task Main(string[] args)
{
    var tasks = new [] { ThrowIndexOfRangeException(), ThrowNullReferenceException() };
    try 
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex) 
    {
        Console.WriteLine(ex);
    }
}
執行結果如下
可以發現 exception 僅僅抓到第一個拋出的例外也就是 indexOutOfRangeException,並不會抓到拋出來的第二個例外,也就是說 catch 抓到的 exception 僅僅是第一個 exception 其餘的都被河蟹掉了(希望的可能是 catch 到所有的例外),有開發者在微軟的 Github 反映 Task.WhenAll - Inner exceptions are lost #31494 此問題,內部有針對此問題做深入討論,原先設計方向是使用 WhenAll 時會發生例外時會將錯誤包在 AggregateException 中,後來因為下列原因修改這設計(怕誤解意思直接使用原文)
a) the vast majority of such cases had fairly homogenous exceptions, such that propagating all in an aggregate wasn't that important 
b) propagating the aggregate then broke expectations around catches for the specific exception types, and 
c) for cases where someone did want the aggregate, they could do so explicitly with the two lines like I wrote.
了解設計初衷之後,如果還是希望在 WhenAll 取得所有的例外可以參考討論串中 noseratio 的回答撰寫 Task 擴充方法蒐集 exception ,代碼如下
public static Task WithAggregatedExceptions(this Task @this)
{
    return @this.ContinueWith(ante =>
        ante.IsFaulted && 
            (ante.Exception.InnerExceptions.Count > 1 || 
            ante.Exception.InnerException is AggregateException) ? 
            Task.FromException(ante.Exception.Flatten()) : 
            ante,
        TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}
加入擴充方法後,下一步就是將既有的 WhenAll 代碼加上 WithAggregatedExceptions 擴充方法
static async Task Main(string[] args)
{
    var tasks = new [] { ThrowIndexOfRangeException(), ThrowNullReferenceException() };
    try
    {
        await Task.WhenAll(tasks).WithAggregatedExceptions();
    }
    catch (Exception ex) 
    {
        Console.WriteLine(ex);
    }
}
再重新看一下 Catch 到的錯誤種類,即可發現被河蟹掉的都找的到了 (大師兄回來了
主線程阻塞
是否會造成主線程雍塞,影響用戶使用
另一個要關注的點是使用後是否會造成阻塞的狀況,由於主線程雍塞的議題已經不是一兩天的事,這裡就簡單整理大神所提到的重點
ASP.NET async 基本心法
閱讀筆記 - 使用 .NET Async/Await 的常見錯誤
.NET 程式鎖死與 SynchronizationContext
或是參考由強者前同事所撰寫的電子書 : .NET 本事-非同步程式設計 :)


感想
魔鬼藏在細節裡。以上就針對 Task.WaitAll 與 Task.WhenAll 做更進一步的說明,以及在討論時所提到的兩個主要的差異內容,這些細節如果一沒有注意到勢必會造成很大的影響,在開發使用也請多加留意或是查相關資料。

參考
C# Thread: What is the difference between Task.WaitAll & Task.WhenAll
Why doesn't await on Task.WhenAll throw an AggregateException?
Task.WhenAll Method
Task.WhenAll - Inner exceptions are lost




2024年1月8日 星期一

[NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll

前言 
在開發偶爾會遇到需要起多個 Task ,接著等待這些 Task 都完成在去做後續邏輯處理,.NET 中提供 Task.WaitAll 與 Task.WhenAll 靜態方法來知道所有任務是否執行完成,過去自己對於兩者的差異性不太明白,因此這篇文章整理自己對於兩者的相關資訊與用法,希望有不清楚或是自己研究錯誤的地方歡迎提出討論

探索問題
Task.WaitAll
在以下的 Sample Code 中使用 Task.Run 建立三個 Task 分別 sleep 1、2、3 秒鐘,接著使用 Task.WaitAll 方法來知道三者是否已執行完成 
static void Main(string[] args)
{
    Task Task1 = Task.Run(() => Thread.Sleep(1000));
    Task Task2 = Task.Run(() => Thread.Sleep(2000));
    Task Task3 = Task.Run(() => Thread.Sleep(3000));

    Task.WaitAll(Task1, Task2, Task3);

    // todo something..
}
在 Task.WaitAll 有提供另一組 API ,可以限定想要等待的時間秒數才不用一直無止境等待下去,這裡在原本的 sample code 再加上等待時間 2.5 秒及透過 Task.IsCompleted 顯示各自是否已完成
static void Main(string[] args)
{
    Task Task1 = Task.Run(() => Thread.Sleep(1000));
    Task Task2 = Task.Run(() => Thread.Sleep(2000));
    Task Task3 = Task.Run(() => Thread.Sleep(3000));

    Task.WaitAll(new Task[] {Task1, Task2, Task3 }, 2500);

    Console.WriteLine("Task1.IsCompleted:{0}", Task1.IsCompleted);
    Console.WriteLine("Task2.IsCompleted:{0}", Task2.IsCompleted);
    Console.WriteLine("Task3.IsCompleted:{0}", Task3.IsCompleted);
}

// result 
// Task1.IsCompleted:True
// Task2.IsCompleted:True
// Task3.IsCompleted:False

在非同步情境時使用 WaitAll 會阻礙執行緒或鎖定 ( blocks thread ),會造成在所有的工作結束之前,當前使用到的執行緒無法自由處理其他工作,在此篇 How and Where Concurrent Asynchronous I/O with ASP.NET Web API 文章有提到,如果某項任務無法正確執行最後引起 deadlocks 狀況發生,此時需要使用 ConfigureAwait 來避免執行緒 lock,更詳細的內容可以參考 Best Practices in Asynchronous Programming

Task.WhenAll
另一個等待所有任務完成的方法是 Task.WhenAll,使用的 Sample Code 中是相同於上述代碼,使用方式不難,在 MSDN Task.Whenall 方法簽章可以看到,使用 Task.WhenAll 方法時會回傳 Task,因此與剛剛差異的是其中等待完成任務方法使用 WhenAll 進行,在使用一個 taskWhenAll 變數用 wait 方法等待完成
static void Main(string[] args)
{
    Task Task1 = Task.Run(() => Thread.Sleep(1000));
    Task Task2 = Task.Run(() => Thread.Sleep(2000));
    Task Task3 = Task.Run(() => Thread.Sleep(3000));

    var taskWhenAll = Task.WhenAll(Task1, Task2, Task3);

    taskWhenAll.Wait();
}
在執行到 Task.WhenAll 時候,會新增一個 Task 並等待該任務的結果 (自己理解上是有專門 Task 在 handle 後續處理),因此使用 WhenAll 不會造成執行緒阻礙的情況發生

Task.WaitAll v.s Task.WhenAll
上面分別介紹兩者的用法與說明,但光看文字與簡單代碼還不過癮,因此小弟參考網路上資料針對 WaitAll 與 WhenAll 執行時間做比較讓數據說話,sample Code 如下
public class Program
{
    static void Main(string[] args)
    {
        IEnumerable<Worker> workerObjects = new List<Worker>
        {
            new Worker {Id = 1, SleepTimeout = 1000},
            new Worker {Id = 2, SleepTimeout = 2000},
            new Worker {Id = 3, SleepTimeout = 3000},
            new Worker {Id = 4, SleepTimeout = 4000},
            new Worker {Id = 5, SleepTimeout = 5000},
        };

        // WaitAll
        TaskWaitAll(workerObjects);

        // WhenAll
        var task = TaskWhenAll(workerObjects);
        
        Console.ReadKey();
    }

    static void TaskWaitAll(IEnumerable<Worker> workers)
    {
        var startTime = DateTime.Now;
        Console.WriteLine("Starting : Task.WaitAll...");

        Task.WaitAll(workers.Select(worker => worker.DoWork(startTime)).ToArray());

        var endTime = DateTime.Now;
        Console.WriteLine("Test finished after {0:F2} seconds.\n", (endTime - startTime).TotalSeconds);
    }

    static Task TaskWhenAll(IEnumerable<Worker> workers)
    {
        var startTime = DateTime.Now;
        Console.WriteLine("Starting test: Task.WhenAll...");

        var task = Task.WhenAll(workers.Select(worker => worker.DoWork(startTime)));
        task.Wait();

        var endTime = DateTime.Now;
        Console.WriteLine("Test finished after {0:F2} seconds.\n", (endTime - startTime).TotalSeconds);

        return task;
    }
}

public class Worker
{
    public int Id;
    public int SleepTimeout;

    public async Task DoWork(DateTime testStart)
    {
        var workerStart = DateTime.Now;
        Console.WriteLine("Worker {0} started on thread {1}, beginning {2:F2} seconds after test start.", Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds);

        await Task.Run(() => Thread.Sleep(SleepTimeout));

        var workerEnd = DateTime.Now;
        Console.WriteLine("Worker {0} stopped; the worker took {1:F2} seconds, and it finished {2:F2} seconds after the test start.", Id, (workerEnd - workerStart).TotalSeconds, (workerEnd - testStart).TotalSeconds);
    }
}
簡單說明 sample code 內容
  • Worker 類別 : 提供 doWork async方法透過 task.run 執行 thread.sleep,可傳入要 sleep 時間與 id,執行前後分別記錄起始、結束時間 
  • main : 主要測試程式進入點,做三件事情
    • 初始化測試的 workerObjects,建立五筆 worker instance 分別測試 1~5 秒
    • 執行測試  TaskWaitAll、TaskWhenAll 方法 
  • TaskWaitAll 方法,裡面紀錄 waitAll 方法起始與結束的時間
  • TaskWhenAll 方法,裡面紀錄 whenAll 方法起始與結束的時間 
執行結果
從以下得知 Task.WaitAll 執行時間為 5.07 秒,Task.WhenAll 執行時間為 5.01 秒
執行多次時間比較都是 WhenAll 會優於 WaitAll,有興趣的客官可以自行下載試試

後記
為了避免執行緒阻塞的情形發生,使用上建議 Task.WhenAll 來取代 Task.WaitAll,從最後的簡單測試代碼執行時間比較來看也是 WhenAll 會優於 WaitAll,其中為了比較 Task.waitAll 與 Task.whenAll 差異性閱讀很多相關的文章與 blog,花了很多時間才產生這篇文章,希望可以透過以上的說明能幫助到有需要的網友(咦 誰是你網友)如果文章中有謬誤或不正確的部分,也請各位大大給予正確指教,最後推薦 MSDN 文章 : Best Practices in Asynchronous Programming 讀完對這方面會很有幫助,謝謝

參考
Async/Await - Best Practices in Asynchronous Programming
Using async/await for multiple tasks
How and Where Concurrent Asynchronous I/O with ASP.NET Web API
await, WhenAll, WaitAll, oh my!!

2023年12月8日 星期五

[conference] .NET Conf 2023 - 使用 .NET 8 建立雲原生應用程序

分享心得
很高興再次有機會可以在 .NET Conf 分享,.NET 8 前陣子推出速度快度讓人驚豔 (各項指標都快到不要不要的),在這次分享中主要介紹 .NET 8 在對於開發者帶來甚麼幫助,自己看完後整理三個重點分別是可觀測性、彈性與新專案 Aspire。 在一年一度的.NET 盛會中,與新舊開發朋友聊聊彼此的狀況,與跟大神們學習新知識,會後將有興趣的議題繼續深入研究,這是自己蠻喜歡的一種方式跟節奏。 以下為這次的投影片,如果在分享內容中有任何錯誤或不清楚之處,歡迎大家提出來進行討論,Happy learning 🙂

議程介紹
主題 : 使用 .NET 8 建立雲原生應用程序
在本次議程中將探討 .NET 8 針對雲原生開發的新功能以及以下主題:
1. 如何透過 .NET 8 的新特性打造高性能和具彈性的服務
2. 開啟系統可觀測性:.NET 8 與 OpenTelemetry 的結合
3. 初探新的分散式應用程式專案 .NET Aspire
這個議程將幫助開發者了解 .NET 8 雲原生開發的新功能,並教您如何運用這些功能來建構現代的、高性能的分佈式應用程序,同時實現可觀測性和彈性。

主辦單位 :study4.tw
議程表 : 連結
共筆 : https://hackmd.io/@Study4/dotnetconf-2023/%2FcSWOXEVMRw695_XhHpQmtA
投影片 : 連結



2023年11月8日 星期三

[conference] ModernWeb 2023 - 壓力測試大冒險:解鎖應用程式的隱藏性能之謎

分享心得
我在 #MWC2023 分享關於壓力測試的那些小事,介紹了壓測中我覺得重要的基本概念,特別是不同的性能測試情境以及它們與服務水平目標 (SLO) 的關係,希望這些觀念對在場的會眾有所幫助 (對於資深的夥伴可能知道的就見笑了 XDDD)。 分享後的問答環節尤其讓人覺得開心,與感興趣的夥伴進行近一個小時的交流,分別討論分享有疑問的地方與自己公司遇到的困難等,在研討會也看到不少老朋友覺得特別的高興。如果在分享內容中有任何錯誤或不清楚之處,歡迎大家提出來進行討論。
會後的問卷調查結果,有機會能夠站在 #MWC2023 台上分享感到很開心,感謝當天議程與會者的建議跟留言,也期許自己未來還有機會分享自己研究的心得 :)


議程介紹
主題 : 壓力測試大冒險:解鎖應用程式的隱藏性能之謎
在本次議程中,我們將全面介紹並分享壓力測試的基本觀念和理念。
1. 我們將深入探究壓力測試的重要性,探討如何明確定義測試目標,並闡述測試如何在應用程式中發現脆弱點,確保系統的穩定性與可靠性。
2. 我們將著重探討壓力測試與服務等級協定(SLA)之間的密不可分聯繫,闡明在確保系統滿足用戶期望方面,壓力測試所扮演的關鍵角色。
3. 我們將透過實際案例與工具演示,深入探討如何精心設計壓力測試場景,模擬各種負載情境,以揭示應用程式的性能極限。這三個核心觀念將引導參與者深入理解壓力測試,揭開優越性能的秘密,同時突顯壓力測試在確保SLA達成中的關鍵作用。

主辦單位 : DevOpsDays Taipei
議程表 : 連結
共筆 : https://hackmd.io/@ModernWeb/2023/%2FMP_4l09JSLKoewO8NxgrnQ
投影片 : 連結



2023年10月10日 星期二

[NETCore] ASP.NET Core 3.0 Worker Service 搭配 Coravel 建立排程服務

前言
如果一直有在 follow 消息的朋友可以發現在 ASP.NET 3.0 有新增 Work Services 專案範本,可以透過幾個簡單的步驟使用 Workers with Windows Services 服務,詳細可以參考微軟官網對於 worker Service 的介紹文章 .NET Core Workers as Windows Services,在上一篇介紹了 ASP.NET Core 中的輕量級排程套件 Coravel,這一篇就來介紹整合 ASP.NET Core Worker Service 與 Coravel 的應用,當然如往常一樣若有問題或是錯誤的地方歡迎網路的高手大大給予指導或討論

建立 Worker Service 專案
在開始之前如果沒有下載 ASP.NET Core 3.0 的朋友,可以到 ASP.NET Core 3.0 下載其 SDK 與相關內容,下載完畢之後接著建立一個名為 WorkerServiceLab 的 ASP.NET Core Application 應用程式專案,在輸入完專案名稱之後在上方選擇 ASP.NET Core 3.0,並且在下方的專案範本選擇 Worker Service,

ASP.NET Core 3.0 的異動 - Program.cs
在開始之前我們先來開啟 program.cs 看一下 worker Service 範本內容
namespace WorkServiceLab
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }
}
如果有在寫 ASP.NET Core 2.2 的朋友可以發現在 createHostBuilder 中 webHostBuilder 已經消失不見,範本中已經改為 HostBuilder,也就是說在 ASP.NET Core 3.0 將會由 Host 取代原來 webHost,這些更新在 pre早期 3.0 preview 2 中有提到未來會以 Generic Host 為主,IWebHostBuilder 並不會就此消失將會繼續保留,詳細細節可以參考 preview2 文章 : 傳送門
如果對於 Generic Host 有興趣,可以參考之前小弟撰寫的相關系列文章

  • ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net - Hosted Builder
  • ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net - Quartz.NET
  • ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net - Windows Service

  • 使用 Coravel
    建立好 worker service 專案,也簡單介紹關於 program.cs 在 ASP.NET Core 3.0 的差異,這邊再繼續介紹 worker service 如何與 coravel 整合的步驟

    安裝 Coravel 套件
    在 ASP.NET Core 中的排程利器 - Coravel 有介紹過基本使用方式,可以透過 Nuget console 進行下載的動作,在 Nuget Package Console 輸入下列指令
    Install-Package Coravel -Version 3.0.0
    安裝完畢之後到專案檔底下確認是否有安裝成功
    <ItemGroup>
      <PackageReference Include="Coravel" Version="3.0.0" />
    </ItemGroup>

    新增 Job 類別
    建立 GetDatetimeJob  類別並實作 IInvocable,代碼如下
    using Coravel.Invocable;
    
    namespace CoravelLab
    {
        public class GetDatetimePreFiveSecondJob : IInvocable
        {
            public Task Invoke()
            {
                Console.WriteLine($"Worker running at: {DateTime.Now}");
                return Task.CompletedTask;  
            }
        }
    }

    CreateHostBuilder 加入 Job
    繼續回到 program.cs 要在 progmram.cs 加入註冊 Job 的動作,因此我們在 ConfigureServices 中透過 AddSchedule 與 AddTransient 來註冊配置要執行的 Job 排程類別
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddScheduler();
                services.AddTransient<GetDatetimeJob>();
            });
    註冊完服務之後接著來設定 Job 執行的頻率,在 Coravel 排程執行時間是透過程式內設定,套件本身建提供多種方法讓開發者設定執行的時間,例如我希望排程要每秒執行一次可以透過 EverySecond() 設定,並且在 Main 加入執行頻率
    public static void Main(string[] args)
    {
        IHost host = CreateHostBuilder(args).Build();
        host.Services.UseScheduler(scheduler =>
        {
            scheduler.Schedule<GetDatetimeJob>()
                .EverySecond();
        });
        host.Run();
    }
    完成上述代碼修改與設定之後,執行專案可以看到輸出結果如下

    加入 Log 及錯誤處理
    在真實的世界中當然不可能像上面介紹那麼簡單,在實務上會在執行時加上 log 機制方便記錄 Job 執行的狀況,這樣可以在執行結果不如預期時或是異常訊息時得知更多的資訊,在 coravel 套件中也支援 log 與 error 處理,我們可以在代碼中加入  LogScheduledTaskProgress  紀錄執行的 log 以及  onError  錯誤處理 
    public static void Main(string[] args)
    {
        IHost host = CreateHostBuilder(args).Build();
        host.Services.UseScheduler(scheduler =>
        {
            scheduler.Schedule<GetDatetimeJob>()
                .EverySecond();
        })
        .LogScheduledTaskProgress(host.Services.GetService<ILogger<IScheduler>>())
        .OnError((exception) =>
            Console.WriteLine(exception.Message)); ;
    
        host.Run();
    }
    這樣在執行過程中發生錯誤才會 catch 到相對應錯誤,如下所示

    Windows Service 
    完成上述建立排程服務的動作下一步就是幫排程 Application 找一個合適的家,在 Windows 中開發完 worker service App 之後可以選擇將應用程式註冊為服務使用,在 ASP.NET Core 2.2 中可能會使用 ServiceBaseLifeTime 在 Windows 服務的生命週期行為進行操作,在 ASP.NET Core 3.0 則可以透過 nuget 安裝  Microsoft.Extensions.Hosting.WindowsServices  來協助此需求
    安裝後就可以在 CreateHostBuilder 內使用 UseWindowsService 在啟動或是停止進行操作或是紀錄 log
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddScheduler();
                //services.AddTransient<GetDatetimeJob>();
            });
        }


    參考
    .NET Core Workers as Windows Services

    Copyright © m@rcus 學習筆記 | Powered by Blogger

    Design by Anders Noren | Blogger Theme by NewBloggerThemes.com