C# Under the Hood: async/await
原文地址:https://www.markopapic.com/csharp-under-the-hood-async-await/
前言
Async 和 await 关键字是在 C# 5 版本中提出的,作为一种很酷的特征用来处理异步任务。它们允许我们以十分简单、直觉的方式来指定将被异步执行的任务。然而,一些人仍然迷惑于异步编程,并且不确定它是如何工作的。我将向你展示当使用 async 和 await 时底下的魔法。
Awaiter Pattern
异步等待者模式
C# 语言编译它一些特征(关键字)称之为语法糖,语法糖的意思是这些语法仅只是现有语法的一种方便的表达。许多这样的语法糖被解释为模式。这些模式都基于方法调用,属性查看或接口实现。 await 表达式就是这些语法糖中的一个。它利用一个基于一些方法调用的模式。为了得到一个可等待的类型,它需要符合以下需求:
它必须具有以下方法:
INotifyCompletion GetAwaiter()
GetAwaiter 方法的返回类型需要实现 INotifyCompletion 接口,并且还要:
- 有一个属性:bool IsCompleted
- 有一个方法:void GetResult()
如果你去看 Task 类的源码,就会发现它符合上述所有需求。
所以,一个类型甚至并不需要实现某些特定的接口才可以成为可等待(类型),只需要有一个特定签名的方法。如图鸭子类(别):
如果一个动物像鸭子一样走路,并且嘎嘎的叫,那它就是一只鸭子。
当前的案例是:
如果一个类型具有特定签名的某个方法,那它就是可等待的。
为了给你一个说明的例子,我将创建一些自定义的类型,并使其可等待。所以,下面是我定义的类:
public class MyAwaitableClass { }
当我尝试等待 MyAwaitableClass 类型的实例,我获得了下面的错误:
它说:‘MyAwaitableClass’ 没有包含 ‘GetAwaiter’ 的定义,没有接受第一个参数是 ‘MyAwaitableClass’ 的 ‘GetAwaiter’ 扩展方法,你是否缺失一个 using 指令或程序集引用?
让我们向我们的类添加一个 GetAwaiter’ 方法:
public class MyAwaitableClass { public MyAwaiter GetAwaiter() { return new MyAwaiter(); } } public class MyAwaiter { public bool IsCompleted { get { return false; } } }
我们可以看到编译器提示错误改不了:
现在它说:‘MyAwaiter’ 没有实现 ‘INotifyCompletion’
好吧,让我们在 MyAwaiter 类中实现 INotifyCompletion 接口:
public class MyAwaiter : INotifyCompletion { public bool IsCompleted { get { return false; } } public void OnCompleted(Action continuation) { } }
接下来,看到的编译器提示错误像这样:
它说:‘MyAwaiter’ 没有包含 ‘GetResult’ 的定义。
所以,我们添加一个 GetResult 方法,于是现在的代码如下:
public class MyAwaitableClass { public MyAwaiter GetAwaiter() { return new MyAwaiter(); } } public class MyAwaiter : INotifyCompletion { public void GetResult() { } public bool IsCompleted { get { return false; } } //From INotifyCompletion public void OnCompleted(Action continuation) { } }
我们将看到这儿编译器没有提示错误:
这意味着我们创建了一个可等待类型。
到此,我们明白了 await 表达式模式,那么我们来看一看 async 和 await 一起使用时到底发生了什么。
Async
异步
对于每一个 async 方法都产生了一个 state machine (状态机)。状态机是一种实现了 来自 System.Runtime.CompilerServices 命名空间IAsyncStateMachine 接口的结构(struct)。这个接口仅只被编译器使用,且有以下方法:
MoveNext() 切换到状态机的下一个状态
SetStateMachine(IAsyncStateMachine) 使用堆中的 IAsyncStateMachine 配置状态机。
现在,让我们来看一下下面的代码:
class Program { static void Main(string[] args) { } static async Task FooAsync() { Console.WriteLine("Async method that doesn't have await"); } }
我们有一个异步的 FooAsync 方法。你可能注意到它缺少 await 操作,但是在当前出于简化的目的我省略了它。
现在我们来看一下编译器将这个方法编译后的代码。我使用 dotPeek 工具来反编译前面生成 dll 文件。为了看到幕布后面的内容,你需要启用 dotPeek 中的 Show Compiler-generated Code 选项。
编译器生成的类通常在类名中含有 < 和 > 符号,这在 C# 标识符中无效,所以这样就不好与用户创建的制品(类型)有冲突。
让我们看一看编译器为我们的 FooAsync 方法生成的内容:
我们的 Program 类包含期望的 Main 和 FooAsync 方法,但是我们也可以看到编译器生成了一个 Progr.<FooAsync>d__1 的结构(struct)。这个结构是一个实现了 IAsyncStateMachine 接口的状态机。除了 IAsyncStateMachine 接口具有的方法外,还具有以下字段:
<>1__state 表明状态机当前的状态
<>t__builder 的类型是 AsyncTaskMethodBuilder ,这个类型被用于创建异步方法和产生用来返回的任务,AsyncTaskMethodBuilder 结构也是出于让编译器使用的。
我们将看到这个结构更加详细的代码,不过在此之前我们先看一下通过反编译出的 FooAsync 方法:
private static Task FooAsync() { Program.<FooAsync>d__1 stateMachine; stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); stateMachine.<>1__state = -1; stateMachine.<>t__builder.Start<Program.<FooAsync>d__1>(ref stateMachine); return stateMachine.<>t__builder.Task; }
这是编译器将异步方法转换后的样子。在方法内部的代码做了以下事情:
- 实例化了这个方法的状态机
- 创建了 AsyncTaskMethodBuilder 实例,并赋值给状态机的 builder 属性
- 设置状态机为开始状态
- 通过调用 Start 方法开始 builder,并传递当前的状态机
- 返回任务
也许你会注意到,编译器产生的 FooAsync 方法没有包含任何我们原始 FooAsync 方法的内容。而原始方法的内容代表着这个方法的功能,那么它去哪儿呢?原始代码内容转移到状态机的 MoveNext 方法中。现在让我们来看一看 Program.<FooAsync>d_1 结构的内容:
[CompilerGenerated] [StructLayout(LayoutKind.Auto)] private struct <FooAsync>d__1 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; void IAsyncStateMachine.MoveNext() { try { Console.WriteLine("Async method that doesn't have await"); } catch (Exception ex) { this.<>1__state = -2; this.<>t__builder.SetException(ex); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { this.<>t__builder.SetStateMachine(stateMachine); } }
MoveNext 方法在 try 块中包含原始方法内容。当一些异常在我们的代码中发生,异常将被给到当前方法中的 builder ,最终传递给任务(FooAsync 方法返回的任务)。在此之后,调用 builder 的 SetResult 方法指出任务已经完成。
现在,我们已经看到异步方法的内幕。出于简洁的目的,我没有在 FooAsync 方法中放置任何的 await 操作,所以我们的状态机没有更多的状态转换。它仅只是执行我们的方法内容并到达完成状态,也就是说,我们的方法是以同步的方法执行。现在是时候看一看,当方法中含有 await 操作时 MoveNext 方法长什么样子。
让我们看一下下面的方法:
static async Task BarAsync() { Console.WriteLine("This happens before await"); int i = await QuxAsync(); Console.WriteLine("This happens after await. The result of await is " + i); }
它等待 QuxAsync 方法并使用这个任务的结果。
如果我们使用 dotPeek 反编译它,我们就会发现编译器产生的代码和之前 FooAsync 方法(编译器产生的代码)具有相同的结果,即便现在的 FooAsync 方法有所不同:
private static Task BarAsync() { Program.<BarAsync>d__2 stateMachine; stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); stateMachine.<>1__state = -1; stateMachine.<>t__builder.Start<Program.<BarAsync>d__2>(ref stateMachine); return stateMachine.<>t__builder.Task; }
而不同之处在于状态机的 MoveNext 方法,现在我们的方法包含 await 表达式,状态机的代码如下:
[CompilerGenerated] [StructLayout(LayoutKind.Auto)] private struct <BarAsync>d__2 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; private TaskAwaiter<int> <>u__1; void IAsyncStateMachine.MoveNext() { int num1 = this.<>1__state; try { TaskAwaiter<int> awaiter; int num2; if (num1 != 0) { Console.WriteLine("This happens before await"); awaiter = Program.QuxAsync().GetAwaiter(); if (!awaiter.IsCompleted) { this.<>1__state = num2 = 0; this.<>u__1 = awaiter; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<BarAsync>d__2>(ref awaiter, ref this); return; } } else { awaiter = this.<>u__1; this.<>u__1 = new TaskAwaiter<int>(); this.<>1__state = num2 = -1; } Console.WriteLine("This happens after await. The result of await is " + (object) awaiter.GetResult()); } catch (Exception ex) { this.<>1__state = -2; this.<>t__builder.SetException(ex); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { this.<>t__builder.SetStateMachine(stateMachine); } }
下面的图片解释了上面状态机:
所以,await 到底做了什么如下所示:
总结
每一次你创建一个异步方法,编译器为其生成一个状态机,然后其中的每一个 await 操作,它做了如下的事情:
- 执行当前方法直到 await 表达式
- 检查当前可等待的任务是否完成
- 如果完成,执行方法中剩下的内容
- 如果没有完成,使用回调去执行方法中剩下的内容,而回调会在可等待任务完成时调用
参考
Task 源码:
https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?redirectedfrom=MSDN&view=netframework-4.8#methods
原文地址:
https://www.markopapic.com/csharp-under-the-hood-async-await/
IAsyncStateMachine 接口
https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.iasyncstatemachine?redirectedfrom=MSDN&view=netframework-4.8
dotPeek
https://www.jetbrains.com/decompiler/
start
https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.asynctaskmethodbuilder.start?redirectedfrom=MSDN&view=netframework-4.8#System_Runtime_CompilerServices_AsyncTaskMethodBuilder_Start__1___0__
SetResult
https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.asynctaskmethodbuilder.setresult?redirectedfrom=MSDN&view=netframework-4.8#System_Runtime_CompilerServices_AsyncTaskMethodBuilder_SetResult