异步编程
发表于:2025-09-10 | 分类: Csharp
字数统计: 2.9k | 阅读时长: 12分钟 | 阅读量:

同步编程

做早餐示例:

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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");
}
onsole.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 提供了一种非阻塞方式来启动任务,然后在任务完成时继续执行。

一个简单的早餐代码的简单异步版本类似于以下片段:

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 后缀。

同时启动任务

对于大多数操作,你希望立即启动多个独立任务。 完成每个任务后,您会开始其他准备好的工作。将此方法应用于早餐示例时,可以更快地准备早餐。你也将一切同时准备好,这样你可以享受热腾腾的早餐。

System.Threading.Tasks.Task 和相关类型可用于将这种推理样式应用于正在进行的任务的类。此方法使你能够编写更类似于你在现实生活中创建早餐的方式的代码。你同时开始煮鸡蛋、煎土豆煎饼和烤面包。当每一种食物都需要进行处理时,你就把注意力转向那个任务,处理相关操作,然后等待其他需要你关注的事情。

在你的代码中,首先启动一个任务,然后保留代表工作的 Task 对象。你对任务使用 await 方法,以推迟处理工作,直到结果准备就绪。

应用于早餐代码。 第一步是在操作开始时存储任务,而不是使用 await 表达式:

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 表达式移到方法末尾,然后再提供早餐:

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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!");

代码修改后可通过减少烹饪时间来改进准备过程,但它们导致鸡蛋和薯饼烧焦,从而引入了一个回归问题。一次性启动所有异步任务。你仅在需要结果时才需要等待每项任务。 该代码可能与 Web 应用程序中的程序类似,它向不同的微服务发出请求,然后将结果合并到单个页面中。立即发出所有请求,然后将 await 表达式应用于所有这些任务,并构建网页。

支持任务组合

以前的代码修订有助于同时准备好早餐的所有部分,但吐司除外。制作吐司的过程是异步操作(烤面包)与同步操作(在吐司上抹黄油和果酱)的组合。此示例说明了有关异步编程的重要概念:

异步操作后跟同步操作的这种组合是一个异步操作。换句话说,如果操作的任何部分是异步的,则整个操作都是异步的。

1
2
3
4
5
6
7
8
// 先烤面包,再涂抹黄油和果酱的任务
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}

修改后的主要代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 方法以匹配以下代码来模拟该问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
输出结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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[] ar

请注意,在烤面包机着火和系统观察异常之间,有相当多的任务完成。当异步运行的任务引发异常时,该任务出错。Task 对象包含 Task.Exception 属性中引发的异常。任务在应用 await 表达式时出错,引发异常。

有两个重要的机制:

  1. 在出错的任务中异常是如何存储的.
  2. 当代码在出错的任务上等待 ( await ) 时,异常是如何被解包并重新引发的.

当运行代码异步引发异常时,异常将存储在对象 Task 中。该 Task.Exception 属性是一个 System.AggregateException 对象,因为异步工作期间可能会引发多个异常。引发的任何异常将添加到 AggregateException.InnerExceptions 集合中。如果该属性中 Exception 为 null,则会创建一个新 AggregateException 对象,并且引发的异常是集合中的第一项。

对于出错的任务,最常见的情况是 Exception 属性只包含一个异常。当代码等待出错任务时,它会重新引发集合中的第一个 AggregateException.InnerExceptions 异常。此结果是示例输出显示对象 System.InvalidOperationException 而不是 AggregateException 对象的原因。提取第一个内部异常让异步方法的使用尽量与同步方法相似。在方案可能生成多个异常时,您可以在代码中检查 Exception 属性。

高效地对任务应用 await 表达式

使用 WhenAll 方法,它会在其 Task 参数列表中的所有任务完成后返回一个完成的对象:

1
2
3
4
5
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 该方法的任务列表中删除已完成的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}

上一篇:
深入异步编程
下一篇:
特殊优化(官方)