iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0

今天會延續昨天的非同步編程繼續下去,那就話不多說直接開始!

支援使用任務(Tasks)進行組合 (Support composition with tasks)

前一個版本的程式碼已經讓早餐的各項準備能夠同時進行,除了吐司之外,製作吐司的過程是由一個非同步操作(烤吐司)與同步操作(抹奶油與果醬)所組成,這個例子展示了一個關於非同步程式設計的重要概念:一個「非同步操作」接著進行「同步操作」的組合,本身仍然是非同步操作,換句話說,只要有任何部分是非同步的,整個作業就是非同步的。
在前面的例子中,已經學會如何使用 TaskTask<TResult> 來儲存執行中的任務,並在使用結果前等待它完成,接下來要做的,是建立代表多個動作組合的方法,在供餐前,你應該等吐司烤好之後,才抹奶油與果醬,這段工作可以用以下程式碼表示:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);

    return toast;
}

MakeToastWithButterAndJamAsync 方法在簽章中使用 async 修飾詞,告訴編譯器這個方法內含 await 敘述並執行非同步操作。此方法代表整個「烤吐司、抹奶油、抹果醬」的任務,並回傳一個 Task<TResult>,代表這三個步驟的組合結果。
修改後的主要程式區塊如下:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    var eggsTask = FryEggsAsync(2);
    var hashBrownTask = FryHashBrownsAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");

    var hashBrown = await hashBrownTask;
    Console.WriteLine("hash browns are ready");

    var toast = await toastTask;
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

這段程式碼變更展示了撰寫非同步程式時的重要技巧:將多個動作組合成一個新的方法,並讓該方法回傳 Task,你可以決定何時等待這個 Task,也可以讓它與其他任務同時啟動。


處理非同步例外狀況 (Handle asynchronous exceptions)

到目前為止,程式假設所有任務都能成功完成。然而,非同步方法也可能像同步方法一樣丟出例外狀況(Exception),非同步的例外處理原則與一般程式設計相同:最佳實踐是讓程式碼的閱讀方式仍像是同步流程。
當任務無法完成時,它會丟出例外;呼叫端可以在 await 該任務時捕捉到例外,例如,在早餐範例中,如果烤麵包機失火,就可以模擬此情況,修改 ToastBreadAsync 如下:

private static async Task<Toast> ToastBreadAsync(int slices)
{
    for (int slice = 0; slice < slices; slice++)
    {
        Console.WriteLine("Putting a slice of bread in the toaster");
    }
    Console.WriteLine("Start toasting...");
    await Task.Delay(2000);
    Console.WriteLine("Fire! Toast is ruined!");
    throw new InvalidOperationException("The toaster is on fire");
    await Task.Delay(1000);
    Console.WriteLine("Remove toast from toaster");

    return new Toast();
}

執行後的輸出結果如下:
Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire

透過結果可以看到,在烤麵包機起火之後,系統察覺例外前,仍有一些任務繼續執行,當一個非同步任務拋出例外時,它會變成「失敗狀態(faulted)」,該任務的例外資訊會儲存在 Task.Exception 屬性中。


有效運用 await(Apply await expressions efficiently)

可以用 Task 類別提供的方法來改進前面程式中一連串的 await。

✅ 使用 Task.WhenAll

此方法會在所有任務完成後才繼續執行:

await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

✅ 使用 Task.WhenAny

此方法在任一任務完成後回傳:

var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
    Task finishedTask = await Task.WhenAny(breakfastTasks);
    if (finishedTask == eggsTask)
        Console.WriteLine("Eggs are ready");
    else if (finishedTask == hashBrownTask)
        Console.WriteLine("Hash browns are ready");
    else if (finishedTask == toastTask)
        Console.WriteLine("Toast is ready");

    await finishedTask; // 仍需 await 以確保例外被正確處理
    breakfastTasks.Remove(finishedTask);
}

Task.WhenAny 會回傳一個「包裝 Task」 (Task<Task>)。你需要再對完成的 finishedTask 使用 await,才能取得結果或確保例外被正確拋出。

最終版本程式碼:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    internal class HashBrown { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static async Task Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            var eggsTask = FryEggsAsync(2);
            var hashBrownTask = FryHashBrownsAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                    Console.WriteLine("eggs are ready");
                else if (finishedTask == hashBrownTask)
                    Console.WriteLine("hash browns are ready");
                else if (finishedTask == toastTask)
                    Console.WriteLine("toast is ready");

                await finishedTask;
                breakfastTasks.Remove(finishedTask);
            }

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);
            return toast;
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
                Console.WriteLine("Putting a slice of bread in the toaster");
            Console.WriteLine("Start toasting...");
            await Task.Delay(3000);
            Console.WriteLine("Remove toast from toaster");
            return new Toast();
        }

        private static async Task<HashBrown> FryHashBrownsAsync(int patties)
        {
            Console.WriteLine($"putting {patties} hash brown patties in the pan");
            Console.WriteLine("cooking first side of hash browns...");
            await Task.Delay(3000);
            for (int patty = 0; patty < patties; patty++)
                Console.WriteLine("flipping a hash brown patty");
            Console.WriteLine("cooking the second side of hash browns...");
            await Task.Delay(3000);
            Console.WriteLine("Put hash browns on plate");
            return new HashBrown();
        }
    }
}

上一篇
Day21-C#使用 async 和 await 進行非同步編程
系列文
30 天從 Python 轉職場 C# 新手入門22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言