windows C#-异步编程概述(三)
任务组合
除了吐司之外,您同时准备好了早餐的所有东西。制作吐司是异步操作(烤面包)和同步操作(添加黄油和果酱)的组合。更新此代码说明了一个重要概念:
异步操作的组合,然后是同步工作,这是异步操作。换句话说,如果操作的任何部分是异步的,则整个操作都是异步的。
上述代码向您展示了您可以使用 Task 或 Task<TResult> 对象来保存正在运行的任务。在使用其结果之前,您需要等待每个任务。下一步是创建表示其他工作组合的方法。在提供早餐之前,您需要等待表示在添加黄油和果酱之前烤面包的任务。您可以使用以下代码表示该工作:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
上述方法的签名中有 async 修饰符。这向编译器发出信号,表示此方法包含一个 await 语句;它包含异步操作。此方法表示烤面包,然后添加黄油和果酱的任务。此方法返回一个 Task<TResult>,表示这三个操作的组合。主要代码块现在变为:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
Console.WriteLine("eggs are ready");
var bacon = await baconTask;
Console.WriteLine("bacon is ready");
var toast = await toastTask;
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
先前的更改说明了使用异步代码的重要技巧。通过将操作分离到返回任务的新方法中,可以编写任务。您可以选择何时等待该任务。您可以同时启动其他任务。
异步异常
到目前为止,您已经隐式假设所有这些任务都已成功完成。异步方法会引发异常,就像它们的同步对应方法一样。对异常和错误处理的异步支持与一般异步支持的目标相同:您应该编写像一系列同步语句一样读取的代码。任务在无法成功完成时会引发异常。客户端代码可以在等待启动的任务时捕获这些异常。例如,假设烤面包机在烤面包时着火。您可以通过修改 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 slices of bacon in the pan
Cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a slice of bacon
Flipping a slice of bacon
Flipping a slice of bacon
Cooking the second side of bacon...
Cracking 2 eggs
Cooking the eggs ...
Put bacon on plate
Put eggs on plate
Eggs are ready
Bacon is ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
at AsyncBreakfast.Program.<Main>(String[] args)
您会注意到,在烤面包机着火和观察到异常之间,有相当多的任务已经完成。当异步运行的任务抛出异常时,该任务就会出错。Task 对象保存在 Task.Exception 属性中抛出的异常。出错的任务在等待时会抛出异常。
需要了解两个重要的机制:异常如何存储在出错的任务中,以及当代码等待出错的任务时如何解包并重新抛出异常。
当异步运行的代码抛出异常时,该异常会存储在 Task 中。Task.Exception 属性是 System.AggregateException,因为在异步工作期间可能会抛出多个异常。任何抛出的异常都会添加到 AggregateException.InnerExceptions 集合中。如果该 Exception 属性为 null,则会创建一个新的 AggregateException,并且抛出的异常是集合中的第一个项目。
出错任务最常见的情况是 Exception 属性只包含一个异常。当代码等待出错的任务时,会重新抛出 AggregateException.InnerExceptions 集合中的第一个异常。这就是为什么此示例的输出显示 InvalidOperationException 而不是 AggregateException。提取第一个内部异常使使用异步方法与使用同步方法尽可能相似。当您的场景可能生成多个异常时,您可以在代码中检查 Exception 属性。
我们建议任何参数验证异常都从任务返回方法中同步出现。
继续之前,请在 ToastBreadAsync 方法中注释掉这两行,否则就再引发另一场灾难:
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");