关于async和await的探讨

缘起

最近在看《深入解析C#(第4版)》这本书,看到了第五章,这一章节是关于异步。之前对异步这个概念只能算是一知半解,了解了它的概念和用法,但是对它的实际场景和为了解决什么问题而诞生的是不太清楚的。于是乎,就和小伙伴之间有了一场讨论。

概念

一般来说对方法的调用都是同步执行的。例如在线程执行体内,即线程的调用函数中,方法的调用就是同步执行的。如果方法需要很长的时间来完成,比方说从Internet加载数据的方法,调用者线程将被阻塞直到方法调用完成。这时候为了避免调用者线程被阻塞,这时候就需要用到异步编程了。异步编程可以解决线程因为等待独占式任务而导致的阻塞问题。

探索

探索过程中,参考了《微软官方文档》,《I/O Threads Explained》。

例子说明

官方以一个做早餐的例子来解释了什么叫同步,并行和异步。

假设做一个早餐需要完成7个步骤:

  1. 倒一杯咖啡。

  2. 加热平底锅,然后煎两个鸡蛋。

  3. 煎三片培根。

  4. 烤两片面包。

  5. 在烤面包上加黄油和果酱。

  6. 倒一杯橙汁。

同步执行

同步执行,是指只有完成上一个任务,才会开始下一个任务;同时将阻塞当前线程执行其他操作,直至任务全部完成

代码例子如下:

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
87
88
89
// 鉴于我用的是vs2022,可能控制台程序的代码在旧版本的vs上无法直接运行,需要补充对应的main函数
MakeBreakfast();

static void MakeBreakfast()
{
    var cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    var eggs = FryEggs(2);
    Console.WriteLine("eggs are ready");

    var bacon = FryBacon(3);
    Console.WriteLine("bacon is ready");

    var toast = ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");

    var oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

static Juice PourOJ()
{
    Console.WriteLine("Pouring orange juice");
    return new Juice();
}

static void ApplyJam(Toast toast) => 
    Console.WriteLine("Putting jam on the toast");

static void ApplyButter(Toast toast) =>
    Console.WriteLine("Putting butter on the toast");

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();
}

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();
}

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();
}

static Coffee PourCoffee()
{
    Console.WriteLine("Pouring coffee");
    return new Coffee();
}

public class Juice { }

public class Bacon { }

public class Egg { }

public class Coffee { }

public class Toast { }

949d65a1323ad42bb042886a144780f3.png

同步执行的总耗时是每个任务耗时的总和。此外,因为是同步执行的原因,在开始制作一份早餐的时候,如果此时又有一份制作早餐的请求过来,是不会开始制作的。如果是客户端程序,使用同步执行耗时时间长的操作,会导致UI线程被阻塞,导致UI线程无法响应用户操作,直至操作完成后,UI线程才相应用户的操作。

异步执行

异步执行,是指在遇到await的时候,才需要等待异步操作完成,然后往下执行;但是不会阻塞当前线程执行其他操作。

代码如下

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
await MakeBreakfastAsync();

static async Task MakeBreakfastAsync()
{
    var cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    var eggs = await FryEggsAsync(2);
    Console.WriteLine("eggs are ready");

    var bacon = await FryBaconAsync(3);
    Console.WriteLine("bacon is ready");

    var toast = await ToastBreadAsync(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");

    var oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

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...");
    Task.Delay(3000).Wait();
    Console.WriteLine("Remove toast from toaster");
    return await Task.FromResult(new Toast());
}

static Task<Bacon> FryBaconAsync(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 Task.FromResult(new Bacon());
}

static Task<Egg> FryEggsAsync(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 Task.FromResult(new Egg());
}

6d469afb67c400ca5a6aa9ba6a3ff39b.png

上面代码只是为了避免堵塞当前的线程,并没有真正用上异步执行的某些关键功能,所以在耗时上是相差不远的;但是这时候如果在接受了一份制作早餐的请求,还未完成的时候,又有一份制作早餐的请求过来,是可能会开始制作另一份早餐的。

改善后的异步执行

代码如下

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
await MakeBreakfastBetterAsync();

static async Task MakeBreakfastBetterAsync()
{
    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!");
}

异步方法的逻辑没有改变,只是调整了一下代码的执行顺序,一开始就调用了三个异步方法,只是在await语句后置了,而不是上面那段代码一样,执行了就在那里等待任务完成,而是会去进行其他的后续操作,直至后续操作需要用到前面任务执行结果的时候,才去获取对应的执行结果,如果没有执行完成就等待执行完成才继续后续的操作。

异步执行并不总是需要另一个线程来执行新任务。并行编程是异步执行的一个子集。

并行编程

并行编程,调用多个线程,同时去执行任务

例如:需要制作五份早餐,同步和异步的方法都是需要循环调用相应的MakeBreakfast方法和MakeBreakfastBetterAsync方法五次才能制作完成。而并行编程,也就是多线程,可以一次性创建五个线程,分别制作一份早餐,从而大大缩短了所需要的时间。

代码如下

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
DateTime beforeDT = DateTime.Now;
for (int i = 0; i < 5; i++)
{
    MakeBreakfast();
}
DateTime afterDT = DateTime.Now;
TimeSpan ts = afterDT.Subtract(beforeDT);
Console.WriteLine($"同步执行程序耗时: {ts.TotalMilliseconds}ms");

beforeDT = DateTime.Now;
for (int i = 0; i < 5; i++)
{
    await MakeBreakfastBetterAsync();
}
afterDT = DateTime.Now;
ts = afterDT.Subtract(beforeDT);
Console.WriteLine($"异步执行程序耗时: {ts.TotalMilliseconds}ms");

beforeDT = DateTime.Now;
await MakeBreakfastBetterMultiTask();
afterDT = DateTime.Now;
ts = afterDT.Subtract(beforeDT);
Console.WriteLine($"并行编程程序耗时: {ts.TotalMilliseconds}ms");

static async Task MakeBreakfastBetterMultiTask()
{
    Task[] tasks = new Task[5];
    for (int i = 0; i < 5; i++)
    {
        tasks[i] = new Task((parameter) => MakeBreakfastBetterAsync().Wait(), "aaa");
        tasks[i].Start();
    }
    Task.WaitAll(tasks);
}

运行耗时结果如下

d6eae350160f8aeb7751e7116e1a4354.png

37f8f64b737948121dac4fbaaa0b0406.png

7c0b61696aba3541b6324b129f7e71b3.png

相比之下,显然能看出来之间的运行耗时差别还是有点大的。

一个通俗的例子

程序就像一个餐馆,线程就像餐馆里面已有的厨师,CPU就是调度厨师的厨师长,假设餐馆开业了,厨师长只带了5个厨师,餐馆接到的订单有8份,同步执行就是5个厨师分别处理5个订单后,这期间,他们会专心的去完成订单的菜,而无视其他的事情,直到完成订单,厨师长才会分配新的订单给他们;异步执行则是5个厨师在处理5个订单的期间,如果厨师长发现他们有人处于空闲状态,就会安排他们去执行剩下3个订单,如果收到等待中的订单可以继续操作时,厨师长会抽调厨师继续完成订单,从而增加了餐馆处理订单的能力。而并行编程则是餐馆开业的时候,告诉了厨师长,需要8个厨师;厨师长就带来了相应数量的厨师来处理订单。

这就是这两天,我对同步,异步和并行之间的感悟。如有不对,敬请指正!

推荐阅读:

Kubernetes全栈架构师(Kubeadm高可用安装k8s集群)--学习笔记

.NET 云原生架构师训练营(模块一 架构师与云原生)--学习笔记

.NET Core开发实战(第1课:课程介绍)--学习笔记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值