同步异步大白话
背景
任务异步编程模型(TAP)提供了对异步代码的抽象。您可以像往常一样,将代码编写为一系列语句。您可以阅读该代码,就好像每条语句都在下一条语句开始之前完成一样。编译器执行许多转换,因为其中一些语句可能开始工作并返回表示正在进行的工作的Task。
这就是该语法的目标:启用读起来像一系列语句的代码,但根据外部资源分配和任务完成时,代码的执行顺序要复杂得多。这类似于人们如何为包含异步任务的流程提供指令。在本文中,您将使用一个制作早餐的指令示例来了解async和wait关键字如何使您更容易推理包含一系列异步指令的代码。你可以写下类似以下列表的说明来解释如何做早餐:
- 倒一杯咖啡。
- 把锅烧热
- 然后煎两个鸡蛋。
- 煎三片培根。
- 烤两片面包。
- 在烤面包上加黄油和果酱。
- 倒一杯橙汁。
如果你有烹饪经验,你会异步执行这些指令。你应该先把锅里的鸡蛋加热,然后再开始培根。你应该把面包放进烤面包机,然后开始煮鸡蛋。在这个过程的每一步,你都会开始一项任务,然后把注意力转移到准备好引起你注意的任务上。
烹饪早餐是异步工作的一个很好的例子。一个人(或线程)可以处理所有这些任务。继续早餐的类比,一个人可以在第一个任务完成之前开始下一个任务,异步地做早餐。不管有没有人在看,烹饪都在进行。一旦你开始加热锅里的鸡蛋,你就可以开始煎培根了。一旦培根开始烤,你就可以把面包放进烤面包机了。
对于并行算法,您需要多个厨师(或线程)。一个做鸡蛋,一个做培根,等等。每一个都只专注于一项任务。每个厨师(或线程)都会被同步阻塞,等待培根准备翻转或烤面包爆裂。
让我们从更新这段代码开始,这样线程在任务运行时就不会阻塞。await关键字提供了一种非阻塞的方式来启动任务,然后在任务完成后继续执行。
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 Bacon { }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");Bacon bacon = FryBacon(3);Console.WriteLine("bacon is 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 Bacon FryBacon(int slices){Console.WriteLine($"putting {slices} slices of bacon in the pan");Console.WriteLine("cooking first side of bacon...");Task.Delay(3000).Wait();for (int slice = 0; slice < slices; slice++){Console.WriteLine("flipping a slice of bacon");}Console.WriteLine("cooking the second side of bacon...");Task.Delay(3000).Wait();Console.WriteLine("Put bacon on plate");return new Bacon();}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();}}
}
不要阻塞,等待
前面的代码演示了一种糟糕的做法:构造同步代码来执行异步操作。正如所写的,这段代码会阻止执行它的线程执行任何其他工作。当任何任务正在进行时,它都不会被中断。就好像你把面包放进去后盯着烤面包机看一样。你会忽略任何和你说话的人,直到烤面包爆裂。
让我们从更新这段代码开始,这样线程在任务运行时就不会阻塞。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!");
}
当鸡蛋或培根烹饪时,此代码不会被阻止。不过,此代码不会启动任何其他任务。你仍然会把烤面包放在烤面包机里,盯着它看,直到它爆裂。但至少,你会回应任何想引起你注意的人。在一家有多份订单的餐厅里,厨师可以在第一份正在烹饪的时候开始另一份早餐。
同时启动任务 Start tasks concurrently
在许多情况下,您希望立即启动几个独立的任务。然后,随着每项任务的完成,您可以继续其他已准备好的工作。在早餐的比喻中,这就是你如何更快地完成早餐的方法。你还可以在几乎同一时间完成所有事情。你会得到一顿热早餐。
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!");
异步准备的早餐的最终版本大约需要6分钟,因为有些任务是并发运行的,并且代码同时监视多个任务,并且只在需要时才采取行动。
最后的代码是异步的。它更准确地反映了一个人如何做早餐。将前面的代码与本文中的第一个代码示例进行比较。通过阅读代码,核心操作仍然清晰可见。您可以像阅读本文开头的早餐制作说明一样阅读此代码。async和await的语言功能为每个人提供了遵循这些书面指令的翻译:尽可能启动任务,不要阻止等待任务完成。
任务组成 Composition with tasks
除了烤面包,你早餐的所有东西都同时准备好了。
烤面包是由异步操作(烤面包)和同步操作(添加黄油和果酱)组成的。更新此代码说明了一个重要概念:
重要的
异步操作和同步工作的组合是异步操作。换句话说,如果操作的任何部分是异步的,那么整个操作就是异步的。
前面的代码向您展示了可以使用Task或Task<TResult>对象来保存正在运行的任务。您等待每个任务,然后再使用其结果。下一步是创建表示其他工作组合的方法。在供应早餐之前,您需要等待代表在添加黄油和果酱之前烤面包的任务。您可以使用以下代码来表示该工作:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{var toast = await ToastBreadAsync(number);ApplyButter(toast);ApplyJam(toast);return toast;
}
前面的更改说明了使用异步代码的一项重要技术。通过将操作分离到返回任务的新方法中,可以组合任务。您可以选择何时等待该任务。您可以同时启动其他任务。
高效地等待任务
前面代码末尾的一系列等待语句可以通过使用Task类的方法进行改进。
- 其中一个API是WhenAll,它返回一个在其参数列表中的所有任务都已完成时完成的Task,如以下代码所示:
await Task.WhenAll(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
- 另一个选项是使用WhenAny,它返回一个Task<Task>,当它的任何参数完成时,它就完成了。
您可以等待返回的任务,知道它已经完成。下面的代码显示了如何使用WhenAny来等待第一个任务完成,然后处理其结果。处理完已完成任务的结果后,将从传递给WhenAny的任务列表中删除该已完成任务。
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{Task finishedTask = await Task.WhenAny(breakfastTasks);if (finishedTask == eggsTask){Console.WriteLine("Eggs are ready");}else if (finishedTask == baconTask){Console.WriteLine("Bacon is ready");}else if (finishedTask == toastTask){Console.WriteLine("Toast is ready");}await finishedTask;breakfastTasks.Remove(finishedTask);
}
现在,在等待任何尚未完成的已启动任务时,处理早餐的线程不会被阻塞。对于某些应用程序,只需要进行此更改。GUI应用程序仍然只通过这个更改来响应用户。但是,对于这种情况,您需要更多。您不希望每个组件任务都按顺序执行。在等待前一个任务完成之前,最好先启动每个组件任务。