02020204 .NET Core重难点知识04-async和await原理揭秘、async背后的线程切换
1. async、await原理揭秘(视频Part2-6)
1.1 源代码
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;namespace Demo02
{class Program{static async Task Main(string[] args){using (HttpClient httpClient = new HttpClient()){string html = await httpClient.GetStringAsync("https://www.baidu.com");Console.WriteLine(html);}string destFilePath = "d:/1.txt";string content01 = "hello async and await";await File.WriteAllTextAsync(destFilePath, content01);string content02 = await File.ReadAllTextAsync(destFilePath);Console.WriteLine("文件内容:" + content02);}}
}
1.2 对上述代码进行反编译

- 使用反编译器,选择C#版本为C# 4.0/VS 2010,可以发现微软将上述代码反编译为了一个类
d__0。 - 编译器将static async Task Main(string[] args) {...}这个方法反编译为了如下两个Main方法。
- 编译器生成的类名都比较怪,避免和我们自己写的类重复。
// Main方法1:<Main>(string[]): void → 无返回值的Main方法,反编译的代码段:
using System.Diagnostics;[DebuggerStepThrough]
private static void <Main>(string[] args)
{Main(args).GetAwaiter().GetResult(); // 调用了返回值为Task的Main方法
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Main方法2:Main(string[]): Task → 返回值为void的异步方法,反编译的代码段:
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;[AsyncStateMachine(typeof(<Main>d__0))]
[DebuggerStepThrough]
private static Task Main(string[] args)
{<Main>d__0 stateMachine = new <Main>d__0(); // 创建了一个<Main>d__0类对象stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();stateMachine.args = args;stateMachine.<>1__state = -1;stateMachine.<>t__builder.Start(ref stateMachine);return stateMachine.<>t__builder.Task;
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
综上:
1. 我们编写代码时声明的static async Task Main(string[] args) {...}这个Main方法,已经不是传统的Main方法了。
2. 真正的Main方法是<Main>(string[]): void。
3. 而Main(string[]): Task是编译器自动生成的
- 查看反编译的
d__0类。
using System;
...
using System.Runtime.CompilerServices;[CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{public int <>1__state;public AsyncTaskMethodBuilder <>t__builder;public string[] args;private string <destFilePath>5__1; // 将方法中的局部变量反编译为了成员变量private string <content01>5__2;...private void MoveNext() // 将static async Task Main(string[] args) {...}这个Main方法切割成了若干块{int num = <>1__state;try{...Console.WriteLine("文件内容" + <content02>5__3);}catch (Exception exception){...}}...
}说明:
1. 关键词:状态机,课后查一下。
2. MoneNext()方法会被调用多次,每一次根据条件的不同,它执行不同的case。
1.3 结论
- 用ILSyp反编译dll(.ext只是Windows下的启动器)成C# 4.0版本,就能看到底层的IL代码。
- await,async是“语法糖”,最终编译成状态机调用。
- async的方法会被C#编译器编译成一个类,会主要根据await调用进行切分为多个状态。
- 对async方法的调用会被拆分为MoveNext的调用。
- 用await看似是等待,经过编译后,其实没有wait。
- 用await看似是等待,其实反编译之后并没有等待。
2. async背后的线程切换(视频Part2-7)
- 抛出两个问题:
- 为何C#编译器要把一个async方法拆分为多个状态然后分为多次调用?
- 异步的可以避免线程等待耗时操作,但是await还是等待?
- 解答:await调用的等待期间,.NET会把当前的线程返回给线程池。当等待的异步方法调用执行完毕之后,框架会从线程池再取出一个线程执行后续的代码。
- Thread.CurrentThread.ManagedThreadId获取当前线程Id,用于验证“在耗时异步(写入大量字符串)操作前后分别打印线程Id“。
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace Demo02
{class Program{static async Task Main(string[] args){Console.WriteLine(Thread.CurrentThread.ManagedThreadId); // 获取写入前线程IdStringBuilder sb = new StringBuilder();for (int i = 0; i < 10000; i++){sb.Append("Qinway");}await File.WriteAllTextAsync(@"..\01.txt", sb.ToString());Console.WriteLine(Thread.CurrentThread.ManagedThreadId); // 获取写入后线程IdConsole.ReadLine();}}
}控制台输出:
1
4说明:
1. 大概率每次运行后线程Id都不一样,如果写入内容少,会发现线程Id不变。
理解 → 点餐时,A服务员引导你做到餐桌上。点餐事件如果比较长,等待期间,A服务员可能去服务其它客人了。当你点餐完成下单时,可能是B服务员来服务你下单。
2. CLR未来的的优化方向:到要等待的时候,如果发现已经执行结束了,那就没有必要再切换线程,剩下的代码就继续在之前的线程上继续执行。
2.1 线程切换并不好,计算机底层在做线程切换的时候,会导致很多资源的切换,性能比较低。
2.2 如果写async和await程序的时候,在不影响系统并发情况下,尽量避免线程切换对系统的并发性能也是有很大帮助的。
2.3 理解这些对后续编写异步代码时很有帮助。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※