感谢书籍《ASP.NET Core 技术内幕与项目实战》及其作者杨中科。
为了更好的解析async和await背后的工作原理我们将上一篇文章:
《C# 中的异步方法中的await和async、Result、 wait()》
其中代码如下:
static async Task Main(string[] args)
{
string filename = "f:/1.txt";
File.Delete(filename);
StringBuilder strb = new StringBuilder();
for (int i = 0; i < 5555; i++)
{
strb.Append(i.ToString());
}
Console.WriteLine("id:"+Thread.CurrentThread.ManagedThreadId+"\r\n");
await File.WriteAllTextAsync(filename, strb.ToString());
Console.WriteLine("id:" + Thread.CurrentThread.ManagedThreadId + "\r\n");
Console.WriteLine(await File.ReadAllTextAsync(filename));
Console.WriteLine("id:" + Thread.CurrentThread.ManagedThreadId + "\r\n");
}
注意反编译时,应选择ConsoleApp1\bin\Debug\netcoreapp3.1目录下的ConsoleApp1.dll而非ConsoleApp1.exe.
·用ILSpy反编译dll(.exe只是windows下的启动器)成C# 4.0版本就能看到容易理解的底层IL代码
观察反编译后Main函数,可看出:
1、Main函数调用了CLR生成的d__0类 。
2、如上图d__0类是一个继承自IAsyncStateMachine,的异步状态机类。
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter<string> awaiter;
TaskAwaiter awaiter2;
if (num != 0)
{
if (num == 1)
{
awaiter = <>u__2;
<>u__2 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
goto IL_01b6;
}
<filename>5__1 = "f:/1.txt";
File.Delete(<filename>5__1);
<strb>5__2 = new StringBuilder();
<i>5__3 = 0;
while (<i>5__3 < 5555)
{
<strb>5__2.Append(<i>5__3.ToString());
<i>5__3++;
}
Console.WriteLine("id:" + Thread.CurrentThread.ManagedThreadId + "\r\n");
awaiter2 = File.WriteAllTextAsync(<filename>5__1, <strb>5__2.ToString()).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter2;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
}
else
{
awaiter2 = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter2.GetResult();
Console.WriteLine("id:" + Thread.CurrentThread.ManagedThreadId + "\r\n");
awaiter = File.ReadAllTextAsync(<filename>5__1).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 1);
<>u__2 = awaiter;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
goto IL_01b6;
IL_01b6:
<>s__4 = awaiter.GetResult();
Console.WriteLine(<>s__4);
<>s__4 = null;
Console.WriteLine("id:" + Thread.CurrentThread.ManagedThreadId + "\r\n");
}
catch (Exception exception)
{
<>1__state = -2;
<filename>5__1 = null;
<strb>5__2 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<filename>5__1 = null;
<strb>5__2 = null;
<>t__builder.SetResult();
}
3、器d__0类的主要函数为MoveNext(),根据异步执行状态更新1__state ,执行不同的处理过程。
4、结论:await、async是“语法糖”,最终编译成“状态机调用”。
在异步程序执行前、后添加:
Console.WriteLine(“id:”+Thread.CurrentThread.ManagedThreadId+“\r\n”);
ThreadCurrentThread.ManagedThreadId获得当前线程Id。验证:在耗时异步(写入大字符串)操作前后分别打印线程Id。
执行程序结果如下:
可看出程序在异步执行前线程ID为1,执行异步时线程ID为4,执行完成后并没有切回ID为1 的线程。
以上代码对于现成的切换仅限于控制台程序,在Winform程序下不会出现线程切换原因详见下文:
GUI 应用运行的时候有默认的 SynchronizationContext(同步上下文)。在异步操作结束的时候, 后续步骤的回调被交给该上下文,以让窗口处理完当前所有消息后执行该回调。这样你会看到异步操作前后都是在该窗口所在的(主)线程上。然而控制台应用并没有默认的 SC。在这种情况下,异步操作结束后回调被交给默认的 TaskScheduler 处理,而默认的 TS 就是调用线程池。于是前后都是在不同的线程上。
由此得出结论:
await调用的等待期间,.NET会把当前的线程返回给线程池等异步方法调用执行完毕后框架会从线程池再取出来一个线程执行后续的代码