.NET进阶

发布时间:
更新时间:
🕒 阅读时间:12 min read 👀 阅读量:Loading...

第二章:异步编程

01-什么是异步

想象一下,你正在做早饭。下面是你需要在做饭的时候完成的一些清单:

  1. 倒一杯咖啡。
  2. 加热锅,然后炒两个鸡蛋。
  3. 烹制三个薯饼。
  4. 烤两块面包。
  5. 将黄油和果酱涂抹在烤面包片上。
  6. 倒一杯橙汁。

你可以按照顺序,从1-6一件件完成这些事情。如果我们在这些任务上标注时间:

  1. 倒一杯咖啡。 (1分钟)
  2. 加热锅,然后炒两个鸡蛋。(5分钟)
  3. 烹制三个薯饼。(5分钟)
  4. 烤两块面包。(8分钟)
  5. 将黄油和果酱涂抹在烤面包片上。(1分钟)
  6. 倒一杯橙汁。(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();来模拟完成任务的时候线程的阻塞情况。

异步顺序执行

但是你在做饭的时候,有些任务你不会傻傻的等着,我列出来这些任务:

  • 加热锅,炒两个鸡蛋

  • 烤两块面包

这些任务你可以等待他们完成,当然也可以在等待的时候做些什么其它的任务,例如:“在锅煎鸡蛋的时候定一个小闹钟/打个便签,然后去干其它的任务,这个闹钟响了,就表明鸡蛋熟了,那我们再把鸡蛋装到盘子上”最终这个煎鸡蛋的任务就完成了。

这里举的例子是微软中的例子,也就是认识异步的概念。

以我个人的理解来说:异步程序不会以阻塞线程的方式来完成任务(在面包机前面傻等),当方法遇到需要等待的操作时,主动释放线程给其他方法,自己暂停等待(不在面包机前)。当等待的操作完成后,方法恢复在线程中继续执行(面包熟了,将面包放在盘子上)。

为了实现上述操作,引入了Taskawait的概念:

  • 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,但是上述代码的执行顺序仍然不是异步的,结果依旧是顺序的:

image-20260527155654299

不过与之前有些不同,举一个例子:

处理 breakfast 的线程,虽然还在按部就班地“一件一件做早餐”,但他的状态变了。

  • 如果按照同步来说,当厨师在烤面包的时候,按照未引入await的状态,厨师只会看着烤箱烤完面包,不会受到外界的任何影响。这个时候如果外界,例如服务员问厨师完成烤面包了吗,厨师是不会回应的。这种叫做线程阻塞。
  • 现在的异步线性版本,厨师把面包按进烤箱(启动了 await Task.Delay)。因为是异步的,厨师现在不用死死盯着烤箱了。他站在原地等面包弹出的这 3 秒内,如果前台服务员喊:“来新订单了!”,厨师可以转过头说:“收到!等我这个面包好了就做!”(这就是对干扰做出响应,线程没有被死锁,应用依然支持用户交互)。

但是,只要没有新的外来订单干扰,这个厨师依然会老老实实地站在原地等面包弹出(线程空出来了,但是没有任务),等拿到了吐司,他才会去开火煎鸡蛋。他自己不会主动去同时开两个炉子。

同时启动任务,实现异步操作

在前面的程序中,都是将Taskawait连接使用,但实际情况下,这两个是可以分开的:

// 连接使用
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("早餐已准备好!");

为了让所有异步任务同时启动,将所有Taskawait分离,让所有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的操作完成后,任务会被重新调度继续执行。多个任务的恢复顺序取决于等待操作何时完成,而不是线程一直在它们之间来回切换。

现在速度稍微会快一些,因为有些操作是在同步运行。

此图显示了准备早餐的说明,其中包括在大约 20 分钟内完成的 8 项异步任务,不幸的是,鸡蛋和土豆饼烤焦了。

问题一:任务异步关系关联

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

02-异步的基础用法

2000年1月1日星期六
00:00:00