.NET进阶
第二章:异步编程
01-什么是异步
想象一下,你正在做早饭。下面是你需要在做饭的时候完成的一些清单:
- 倒一杯咖啡。
- 加热锅,然后炒两个鸡蛋。
- 烹制三个薯饼。
- 烤两块面包。
- 将黄油和果酱涂抹在烤面包片上。
- 倒一杯橙汁。
你可以按照顺序,从1-6一件件完成这些事情。如果我们在这些任务上标注时间:
- 倒一杯咖啡。 (1分钟)
- 加热锅,然后炒两个鸡蛋。(5分钟)
- 烹制三个薯饼。(5分钟)
- 烤两块面包。(8分钟)
- 将黄油和果酱涂抹在烤面包片上。(1分钟)
- 倒一杯橙汁。(1分钟)
那么按照顺序来说,你的总用时为:1+5+5+8+1+1 = 21分钟。这种执行任务的方式在编程中称之为同步运行模式,也就是顺序执行每一项任务。下面是同步完成这些任务的代码实例:
using System;using System.Threading.Tasks;
namespace AsyncBreakfast{ // 这些类有意留空,仅用于示例目的。它们只是用于演示的标记类,不包含任何属性,也没有其他用途。 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("咖啡已准备就绪");
Egg eggs = FryEggs(2); Console.WriteLine("鸡蛋已准备就绪");
HashBrown hashBrown = FryHashBrowns(3); Console.WriteLine("薯饼已准备就绪");
Toast toast = ToastBread(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine("吐司已准备就绪");
Juice oj = PourOJ(); Console.WriteLine("橙汁已准备就绪"); Console.WriteLine("早餐已全部准备完毕!"); }
private static Juice PourOJ() { Console.WriteLine("正在倒橙汁"); return new Juice(); }
private static void ApplyJam(Toast toast) => Console.WriteLine("正在给吐司抹果酱");
private static void ApplyButter(Toast toast) => Console.WriteLine("正在给吐司抹黄油");
private static Toast ToastBread(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine("将一片面包放入烤面包机"); } Console.WriteLine("开始烤面包..."); Task.Delay(3000).Wait(); Console.WriteLine("从烤面包机中取出吐司");
return new Toast(); }
private static HashBrown FryHashBrowns(int patties) { Console.WriteLine($"将 {patties} 个薯饼放入锅中"); Console.WriteLine("正在煎薯饼的第一面..."); Task.Delay(3000).Wait(); for (int patty = 0; patty < patties; patty++) { Console.WriteLine("翻面煎薯饼"); } Console.WriteLine("正在煎薯饼的第二面..."); Task.Delay(3000).Wait(); Console.WriteLine("将薯饼盛到盘子里");
return new HashBrown(); }
private static Egg FryEggs(int howMany) { Console.WriteLine("正在加热煎蛋锅..."); Task.Delay(3000).Wait(); Console.WriteLine($"打了 {howMany} 个鸡蛋"); Console.WriteLine("正在煎蛋..."); Task.Delay(3000).Wait(); Console.WriteLine("将煎蛋盛到盘子里");
return new Egg(); }
private static Coffee PourCoffee() { Console.WriteLine("正在倒咖啡"); return new Coffee(); } }}这里使用Task.Delay(3000).Wait();来模拟完成任务的时候线程的阻塞情况。
异步顺序执行
但是你在做饭的时候,有些任务你不会傻傻的等着,我列出来这些任务:
-
加热锅,炒两个鸡蛋
-
烤两块面包
这些任务你可以等待他们完成,当然也可以在等待的时候做些什么其它的任务,例如:“在锅煎鸡蛋的时候定一个小闹钟/打个便签,然后去干其它的任务,这个闹钟响了,就表明鸡蛋熟了,那我们再把鸡蛋装到盘子上”最终这个煎鸡蛋的任务就完成了。
这里举的例子是微软中的例子,也就是认识异步的概念。
以我个人的理解来说:异步程序不会以阻塞线程的方式来完成任务(在面包机前面傻等),当方法遇到需要等待的操作时,主动释放线程给其他方法,自己暂停等待(不在面包机前)。当等待的操作完成后,方法恢复在线程中继续执行(面包熟了,将面包放在盘子上)。
为了实现上述操作,引入了Task和await的概念:
- Task:异步操作的表示。返回 Task 或 Task
的方法称之为异步方法。 - await:遇到这个关键字会暂停当前方法,释放线程,等待后面的 Task 完成。
using System;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("咖啡已准备就绪");
// 线性异步:虽然线程没死锁,但依然在“排队”做 Egg eggs = await FryEggsAsync(2); Console.WriteLine("鸡蛋已准备就绪");
HashBrown hashBrown = await FryHashBrownsAsync(3); Console.WriteLine("薯饼已准备就绪");
Toast toast = await ToastBreadAsync(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine("吐司已准备就绪");
Juice oj = PourOJ(); Console.WriteLine("橙汁已准备就绪"); Console.WriteLine("早餐已全部准备完毕!"); }
private static Juice PourOJ() { Console.WriteLine("正在倒橙汁"); return new Juice(); }
private static void ApplyJam(Toast toast) => Console.WriteLine("正在给吐司抹果酱");
private static void ApplyButter(Toast toast) => Console.WriteLine("正在给吐司抹黄油");
private static async Task<Toast> ToastBreadAsync(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine("将一片面包放入烤面包机"); } Console.WriteLine("开始烤面包..."); await Task.Delay(3000); Console.WriteLine("从烤面包机中取出吐司");
return new Toast(); }
private static async Task<HashBrown> FryHashBrownsAsync(int patties) { Console.WriteLine($"将 {patties} 个薯饼放入锅中"); Console.WriteLine("正在煎薯饼的第一面..."); await Task.Delay(3000); for (int patty = 0; patty < patties; patty++) { Console.WriteLine("翻面煎薯饼"); } Console.WriteLine("正在煎薯饼的第二面..."); await Task.Delay(3000); Console.WriteLine("将薯饼盛到盘子里");
return new HashBrown(); }
private static async Task<Egg> FryEggsAsync(int howMany) { Console.WriteLine("正在加热煎蛋锅..."); await Task.Delay(3000); Console.WriteLine($"打了 {howMany} 个鸡蛋"); Console.WriteLine("正在煎蛋..."); await Task.Delay(3000); Console.WriteLine("将煎蛋盛到盘子里");
return new Egg(); }
private static Coffee PourCoffee() { Console.WriteLine("正在倒咖啡"); return new Coffee(); } }}虽然引入了await,但是上述代码的执行顺序仍然不是异步的,结果依旧是顺序的:

不过与之前有些不同,举一个例子:
处理 breakfast 的线程,虽然还在按部就班地“一件一件做早餐”,但他的状态变了。
- 如果按照同步来说,当厨师在烤面包的时候,按照未引入await的状态,厨师只会看着烤箱烤完面包,不会受到外界的任何影响。这个时候如果外界,例如服务员问厨师完成烤面包了吗,厨师是不会回应的。这种叫做线程阻塞。
- 现在的异步线性版本,厨师把面包按进烤箱(启动了
await Task.Delay)。因为是异步的,厨师现在不用死死盯着烤箱了。他站在原地等面包弹出的这 3 秒内,如果前台服务员喊:“来新订单了!”,厨师可以转过头说:“收到!等我这个面包好了就做!”(这就是对干扰做出响应,线程没有被死锁,应用依然支持用户交互)。
但是,只要没有新的外来订单干扰,这个厨师依然会老老实实地站在原地等面包弹出(线程空出来了,但是没有任务),等拿到了吐司,他才会去开火煎鸡蛋。他自己不会主动去同时开两个炉子。
同时启动任务,实现异步操作
在前面的程序中,都是将Task和await连接使用,但实际情况下,这两个是可以分开的:
// 连接使用Coffee cup = PourCoffee();Console.WriteLine("咖啡已准备好");
Task<Egg> eggsTask = FryEggsAsync(2);Egg eggs = await eggsTask; // 和task连Console.WriteLine("鸡蛋已准备好");
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);HashBrown hashBrown = await hashBrownTask;// 和task连Console.WriteLine("薯饼已准备好");
Task<Toast> toastTask = ToastBreadAsync(2);Toast toast = await toastTask;// 和task连ApplyButter(toast);ApplyJam(toast);Console.WriteLine("吐司已准备好");
Juice oj = PourOJ();Console.WriteLine("橙汁已准备好");Console.WriteLine("早餐已准备好!");为了让所有异步任务同时启动,将所有Task和await分离,让所有Task一起启动,并在合适的位置进行await等待。
// 代码逻辑在死等吐司! 有烧焦风险Coffee cup = PourCoffee();Console.WriteLine("咖啡已准备好");
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("吐司已准备好");Juice oj = PourOJ();Console.WriteLine("橙汁已准备好");
Egg eggs = await eggsTask;Console.WriteLine("鸡蛋已准备好");HashBrown hashBrown = await hashBrownTask;Console.WriteLine("薯饼已准备好");
Console.WriteLine("早餐已准备好!");现在是多个异步任务同时启动,每个任务执行到 await 时会暂停并释放线程;当await的操作完成后,任务会被重新调度继续执行。多个任务的恢复顺序取决于等待操作何时完成,而不是线程一直在它们之间来回切换。
现在速度稍微会快一些,因为有些操作是在同步运行。

问题一:任务异步关系关联
问题二:在执行新的任务时,之前await的任务需要的数据传输到了,这个时候异步程序会如何