不要阻塞,而要等待
上述代码演示了一种不好的做法:构建同步代码来执行异步操作。正如所写,此代码会阻止执行它的线程执行任何其他工作。在任何任务正在进行时,它都不会被中断。这就像你把面包放进去后盯着烤面包机一样。你会忽略任何和你说话的人,直到烤面包机爆开。
让我们首先更新此代码,以便线程在任务运行时不会阻塞。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");Bacon bacon = await FryBaconAsync(3);Console.WriteLine("bacon is 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!");
}
总耗时与初始同步版本大致相同。代码尚未利用异步编程的一些关键功能。
FryEggsAsync、FryBaconAsync 和 ToastBreadAsync 的方法主体都已更新,分别返回 Task<Egg>、Task<Bacon> 和 Task<Toast>。这些方法从其原始版本重命名为包含“Async”后缀。它们的实现将在本文后面的最终版本中显示。
Main 方法返回 Task,尽管没有返回表达式 - 这是设计使然。有关更多信息,请参阅返回 void 的异步函数的求值。
此代码在鸡蛋或培根烹饪时不会阻塞。但此代码不会启动任何其他任务。您仍会将面包放入烤面包机并盯着它直到它爆开。但至少,您会回应任何想要您注意的人。在一家有多个订单的餐厅中,厨师可以在第一个早餐烹饪的同时开始另一个早餐。
现在,在等待任何尚未完成的已启动任务时,处理早餐的线程不会被阻塞。对于某些应用程序,此更改就是所需的全部。GUI 应用程序仍会通过此更改来响应用户。但是,对于这种情况,您需要更多。您不希望每个组件任务按顺序执行。最好在等待上一个任务完成之前启动每个组件任务。
同时启动任务
在许多情况下,您希望立即启动几个独立的任务。然后,当每个任务完成时,您可以继续其他已准备好的工作。在早餐类比中,这就是您更快地完成早餐的方式。您还可以在接近同一时间完成所有工作。您将获得一顿热早餐。
System.Threading.Tasks.Task 和相关类型是您可以用来推理正在进行的任务的类。这使您可以编写更接近制作早餐的方式的代码。您将同时开始煮鸡蛋、培根和吐司。由于每个任务都需要采取行动,因此您将把注意力转向该任务,处理下一个操作,然后等待需要您注意的其他事情。
您启动一项任务并保留代表该工作的 Task 对象。您将等待每个任务,然后再处理其结果。
让我们对早餐代码进行以下更改。第一步是存储任务以在操作开始时进行操作,而不是等待它们:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is 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 语句移到方法的末尾,然后再提供早餐:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(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");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");Console.WriteLine("Breakfast is ready!");
异步准备的早餐大约需要 20 分钟,节省的时间是因为一些任务是并发运行的。
上述代码效果更好。您可以一次启动所有异步任务。只有在需要结果时才等待每个任务。上述代码可能类似于 Web 应用程序中的代码,该代码向不同的微服务发出请求,然后将结果合并到单个页面中。您将立即发出所有请求,然后等待所有这些任务并编写网页。