02020204 .NET Core重难点知识04-async和await原理揭秘、async背后的线程切换

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); // 获取写入前线程Id
            StringBuilder 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); // 获取写入后线程Id

            Console.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※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-08-24 22:02  qinway  阅读(6)  评论(0)    收藏  举报