Task 异步编程模型(TAP)提供了一层抽象,以覆盖典型的异步编码。 在此模型中,将代码编写为语句序列,与往常相同。 关键区别是,在编译器处理每个语句的过程中以及开始处理下一个语句之前,您都可以读取基于任务的代码。 若要完成此模型,编译器会执行许多转换来完成每个任务。 某些语句可以启动工作并返回表示 Task 正在进行的工作的对象,编译器必须解析这些转换。 任务异步编程的目标是使代码看起来像一系列语句那样易读,但能够以更复杂的顺序执行。 执行基于外部资源分配以及任务完成时间。
任务异步编程模型类似于人们如何为包含异步任务的进程提供说明。 本文通过一个制作早餐说明的示例,展示了如何使用 async
和 await
关键字,使包含一系列异步指令的代码更容易理解。 提供早餐的说明可以作为列表提供:
- 倒一杯咖啡。
- 加热锅,然后炒两个鸡蛋。
- 烹制三个薯饼。
- 烤两块面包。
- 将黄油和果酱涂抹在烤面包片上。
- 倒一杯橙汁。
如果你有烹饪经验,则可以 异步完成这些说明。 你开始为煎鸡蛋加热锅,然后开始煮土豆饼。 你把面包放在烤箱里,然后开始煮鸡蛋。 在流程的每个步骤中,你启动一个任务,然后转向需要你注意的其他任务。
烹饪早餐是异步而非并行工作的一个很好的示例。 一个人(或线程)可以处理所有任务。 一个人可以通过在上一个任务完成之前启动下一个任务来异步做早餐。 不论是否有人在观看,每个烹饪任务都会继续进行。 在开始加热平底锅准备煎蛋的同时就可以开始烹制薯饼。 薯饼开始煎煮时,您可以将面包放入烤面包机。
对于并行算法,需要多个厨师(或多个线程)。 一个人负责煎鸡蛋,另一个人负责煎土豆饼,依此类推。 每个人专注于他们的一个特定任务。 每个正在做饭的人(或每个线程)都被同步阻止,等待当前任务完成:准备翻转薯饼、准备在烤面包机中弹出面包等等。
请考虑相同的同步指令列表,以 C# 代码语句的形式呈现:
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class HashBrown { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = FryHashBrowns(3);
Console.WriteLine("hash browns are ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
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 Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static HashBrown FryHashBrowns(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
Task.Delay(3000).Wait();
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
Task.Delay(3000).Wait();
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
如果像计算机一样解释这些说明,准备早餐需要大约30分钟。 持续时间是单个任务时间的总和。 计算机会暂停处理每个语句,直到所有工作完成,然后继续执行下一个任务。 此方法可能需要很长时间。 在早餐示例中,计算机技术创建了一个令人不满意的早餐。 同步列表中的后续任务,比如烤面包,必须等到早期任务完成后才能开始。 一些食物在早餐准备好供应之前变冷。
如果希望计算机异步执行指令,则必须编写异步代码。 编写客户端程序时,希望 UI 能够响应用户输入。 从 Web 下载数据时,应用程序不应冻结所有交互。 编写服务器程序时,不希望阻止可能正在处理其他请求的线程。 存在异步替代项的情况下使用同步代码会增加你进行扩展的成本。 你需要为受阻线程付费。
成功的新式应用需要异步代码。 如果没有语言支持,编写异步代码需要回调、完成事件或其他掩盖代码的原始意图。 同步代码的优点是分步作,便于扫描和理解。 传统的异步模型强制你专注于代码的异步性质,而不是关注代码的基本作。
不要阻塞,改为等待
前面的代码重点介绍了一种不幸的编程做法:编写同步代码以执行异步作。 代码阻止当前线程执行任何其他工作。 代码不会在运行任务时中断线程。 这种模型的结果就好比你把面包放进烤面包机后就一直盯着它。 你忽略所有干扰,在面包弹出之前不会开始其他任务。 你不会从冰箱里拿出黄油和果酱。 你可能会看不到炉灶上起火。 你其实想在烤面包的同时处理其他事情。 代码也是同样的道理。
可以先更新代码,这样线程就不会在任务运行时阻止。 关键字 await
提供一种非阻止方式来启动任务,然后在任务完成时继续执行。 一个简单的早餐代码的简单异步版本类似于以下片段:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = await FryEggsAsync(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = await FryHashBrownsAsync(3);
Console.WriteLine("hash browns are ready");
Toast toast = await ToastBreadAsync(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
代码更新了原始方法正文FryEggs
、FryHashBrowns
和ToastBread
,使它们分别返回Task<Egg>
、Task<HashBrown>
和Task<Toast>
对象。 更新的方法名称包括“Async”后缀: FryEggsAsync
、 FryHashBrownsAsync
和 ToastBreadAsync
。 即使该方法没有 Main
表达式,但根据设计,它会返回 Task
对象。 有关详细信息,请参阅 对返回 void 的异步函数的评估。
注释
更新的代码尚未利用异步编程的关键功能,这可能会导致完成时间缩短。 代码在与初始同步版本大致相同的时间内处理任务。 有关完整的方法实现,请参阅本文后面的 代码的最终版本 。
让我们将早餐示例应用于更新的代码。 线程在煎鸡蛋或土豆饼时不会被阻塞,但在当前工作完成之前,代码也不会启动其他任务。 你仍然会把面包放进烤面包机,然后盯着烤面包机直到面包弹出,但现在你可以对干扰做出响应。 在一家收到多个订单的餐厅里,厨师可以在烹饪一个订单的食物时,开始处理新订单。
在更新的代码中,处理早餐的线程在等待任何未完成的已启动任务时不会被阻塞。 对于某些应用程序,只需进行此更改。 可以在从 Web 下载数据时使应用支持用户交互。 在其他方案中,你可能希望在等待上一个任务完成时启动其他任务。
同时启动任务
对于大多数操作,你希望立即启动多个独立任务。 完成每个任务后,您会开始其他准备好的工作。 将此方法应用于早餐示例时,可以更快地准备早餐。 你也将一切同时准备好,这样你可以享受热腾腾的早餐。
类 System.Threading.Tasks.Task 和相关类型是可用于将这种推理样式应用于正在进行的任务的类。 此方法使你能够编写更类似于你在现实生活中创建早餐的方式的代码。 你同时开始煮鸡蛋、煎土豆煎饼和烤面包。 当每一种食物都需要进行处理时,你就把注意力转向那个任务,处理相关操作,然后等待其他需要你关注的事情。
在你的代码中,首先启动一个任务,然后保留代表工作的 Task 对象。 你对任务使用 await
方法,以推迟处理工作,直到结果准备就绪。
将这些更改应用于早餐代码。 第一步是在操作开始时存储任务,而不是使用 await
表达式。
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");
这些修订并不能帮助更快地准备早餐。 表达式 await
在启动后立即应用于所有任务。 下一步是将薯饼和鸡蛋的 await
表达式移到方法末尾,然后再提供早餐:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Breakfast is ready!");
现在你可以通过异步方式准备早餐,大约需要 20 分钟。 烹饪总时间会减少,因为某些任务同时运行。
代码更新可通过减少烹饪时间来改进准备过程,但它们导致鸡蛋和薯饼烧焦,从而引入了一个回归问题。 一次性启动所有异步任务。 你仅在需要结果时才需要等待每项任务。 该代码可能与 Web 应用程序中的程序类似,它向不同的微服务发出请求,然后将结果合并到单个页面中。 立即发出所有请求,然后将 await
表达式应用于所有这些任务,并构建网页。
支持任务组合
以前的代码修订有助于同时准备好早餐的所有部分,但吐司除外。 制作吐司的过程是异步操作(烤面包)与同步操作(在吐司上抹黄油和果酱)的组合。 此示例说明了有关异步编程的重要概念:
重要
异步操作后跟同步操作的这种组合是一个异步操作。 换句话说,如果操作的任何部分是异步的,则整个操作都是异步的。
在以前的更新中,你了解了如何使用 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!");
}
此代码更改演示了处理异步代码的重要技术。 你可以通过将操作分离到一个返回任务的新方法中来组合任务。 你可以选择何时等待该任务完成。 可以同时启动其他任务。
处理异步异常
至此,代码隐式假定所有任务都成功完成。 异步方法会引发异常,就像其同步方法一样。 异步支持异常和错误处理的目标与一般异步支持相同。 最佳做法是编写类似于一系列同步语句的代码。 当任务无法成功完成时,它们将引发异常。 当表达式应用于启动的任务时, 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
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 属性中引发的异常。 任务在应用 await
表达式时出错,引发异常。
有两个重要的机制可以了解此过程:
- 在出错的任务中异常是如何存储的
- 当代码在出错的任务上等待 (
await
) 时,异常是如何被解包并重新引发的
当运行代码异步引发异常时,异常将存储在对象中 Task
。 该 Task.Exception 属性是一个 System.AggregateException 对象,因为异步工作期间可能会引发多个异常。 引发的任何异常将添加到 AggregateException.InnerExceptions 集合中。 如果该属性为 Exception
null,则会创建一个新 AggregateException
对象,并且引发的异常是集合中的第一项。
对于出错的任务,最常见的情况是 Exception
属性只包含一个异常。 当代码等待出错任务时,它会重新引发集合中的第一个 AggregateException.InnerExceptions 异常。 此结果是示例输出显示对象 System.InvalidOperationException 而不是 AggregateException
对象的原因。 提取第一个内部异常让异步方法的使用尽量与同步方法相似。 在方案可能生成多个异常时,您可以在代码中检查 Exception
属性。
小窍门
建议的做法是使任何参数验证异常在任务返回方法中 同步 出现。 有关详细信息和示例,请参阅 任务返回方法中的异常。
在继续下一部分之前,请在您的ToastBreadAsync
方法中注释掉以下两个语句。 你不想启动另一个火灾:
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
高效地对任务应用 await 表达式
可以通过使用await
类的方法改进上一个代码末尾的Task
系列表达式。 一个 API 是使用 WhenAll 方法,它会在其 Task 参数列表中的所有任务完成后返回一个完成的对象。 以下代码演示了此方法:
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!");
另一个选项是使用这个 WhenAny 方法,该方法返回一个当其任一参数完成时就完成的 Task<Task>
对象。 你可以等待任务返回,因为你知道任务已经完成了。 以下代码演示如何使用 WhenAny 该方法等待第一个任务完成,然后处理其结果。 处理已完成任务的结果后,将从传递给 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;
breakfastTasks.Remove(finishedTask);
}
在代码片段的末尾附近,请注意表达式 await finishedTask;
。 表达式 await Task.WhenAny
不会等待完成的任务,而是等待 Task
方法返回 Task.WhenAny
的对象。 Task.WhenAny
方法的结果是已完成或已出错的任务。 最佳做法是再次等待任务,即使知道任务已完成。 通过这种方式,你可以检索任务结果,或者确保导致任务出错的任何异常都能被引发。
查看最终代码
下面是代码的最终版本:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
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();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
代码在大约 15 分钟内完成异步早餐任务。 由于某些任务同时运行,因此总时间会减少。 该代码同时监视多个任务,并仅根据需要执行作。
最终代码是异步的。 它更准确地反映了一个人做早餐的方式。 将最终代码与本文中的第一个代码示例进行比较。 通过阅读代码,核心动作仍然清晰。 可以像阅读早餐说明列表一样阅读最终代码,如文章开头所示。 async
和 await
关键字的语言特性实现了人人都能理解的编程范式转换:尽可能地启动任务,不要在等待任务完成时造成阻塞。
Async/await 和 ContinueWith
async
和 await
关键字提供了语法简化,而不是直接使用 Task.ContinueWith。 虽然async
/await
并且ContinueWith
具有类似的语义来处理异步作,但编译器不一定直接将表达式转换为await
ContinueWith
方法调用。 相反,编译器会生成提供相同逻辑行为的优化状态机代码。 此转换提供显著的可读性和可维护性优势,尤其是在链接多个异步作时。
假设在某个场景下,需要您执行多个顺序异步操作。 下面是使用 ContinueWith
实现时的逻辑与使用 async
/await
实现的逻辑的比较:
使用 ContinueWith
使用 ContinueWith
时,一个异步操作序列中的每个步骤都需要嵌套的连续操作:
// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
return StartCookingEggsAsync()
.ContinueWith(eggsTask =>
{
var eggs = eggsTask.Result;
Console.WriteLine("Eggs ready, starting bacon...");
return StartCookingBaconAsync();
})
.Unwrap()
.ContinueWith(baconTask =>
{
var bacon = baconTask.Result;
Console.WriteLine("Bacon ready, starting toast...");
return StartToastingBreadAsync();
})
.Unwrap()
.ContinueWith(toastTask =>
{
var toast = toastTask.Result;
Console.WriteLine("Toast ready, applying butter...");
return ApplyButterAsync(toast);
})
.Unwrap()
.ContinueWith(butteredToastTask =>
{
var butteredToast = butteredToastTask.Result;
Console.WriteLine("Butter applied, applying jam...");
return ApplyJamAsync(butteredToast);
})
.Unwrap()
.ContinueWith(finalToastTask =>
{
var finalToast = finalToastTask.Result;
Console.WriteLine("Breakfast completed with ContinueWith!");
});
}
使用 async/await
使用 async
/await
执行相同操作序列时更为自然:
// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
var eggs = await StartCookingEggsAsync();
Console.WriteLine("Eggs ready, starting bacon...");
var bacon = await StartCookingBaconAsync();
Console.WriteLine("Bacon ready, starting toast...");
var toast = await StartToastingBreadAsync();
Console.WriteLine("Toast ready, applying butter...");
var butteredToast = await ApplyButterAsync(toast);
Console.WriteLine("Butter applied, applying jam...");
var finalToast = await ApplyJamAsync(butteredToast);
Console.WriteLine("Breakfast completed with async/await!");
}
为什么首选 async/await
此方法 async
/await
提供以下几个优点:
- 可读性:代码读取方式类似于同步代码,因此更易于理解作流。
- 可维护性:添加或删除序列中的步骤需要最少的代码更改。
- 错误处理:使用
try
/catch
块的异常处理自然有效,而ContinueWith
需要仔细处理出错的任务。 - 调试:使用
async
/await
后,调用堆栈和调试器的体验有所提升。 - 性能:编译器优化
async
/await
比手动ContinueWith
链更复杂。
随着链式操作的数量增加,好处变得更加明显。 尽管单个延续可以使用ContinueWith
进行管理,但 3-4 个或更多异步操作的序列很快就会变得难以阅读和维护。 此模式在函数编程中称为“monadic do-notation”,允许以顺序、可读的方式组合多个异步操作。