今天會延續昨天的非同步編程繼續下去,那就話不多說直接開始!
前一個版本的程式碼已經讓早餐的各項準備能夠同時進行,除了吐司之外,製作吐司的過程是由一個非同步操作(烤吐司)與同步操作(抹奶油與果醬)所組成,這個例子展示了一個關於非同步程式設計的重要概念:一個「非同步操作」接著進行「同步操作」的組合,本身仍然是非同步操作,換句話說,只要有任何部分是非同步的,整個作業就是非同步的。
在前面的例子中,已經學會如何使用 Task
或 Task<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,也可以讓它與其他任務同時啟動。
到目前為止,程式假設所有任務都能成功完成。然而,非同步方法也可能像同步方法一樣丟出例外狀況(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 屬性中。
可以用 Task 類別提供的方法來改進前面程式中一連串的 await。
此方法會在所有任務完成後才繼續執行:
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!");
此方法在任一任務完成後回傳:
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();
}
}
}