C# Async/Await原理剖析
什么是Async/Await
Async/Await是C# 5引入的关键字,用以提高用户界面响应能力和对Web资源的访问能力,同时它使异步代码的编写变得更加容易。
为什么需要Async/Await
1. 需要使用异步编程技术来提高程序的响应能力
在Windows桌面应用中,当click download button时,我们不希望其导致整个ui失去响应。我们希望ui线程能够实时的响应窗体事件,而不会被下载等耗时的IO操作所阻塞。这就需要我们使用异步编程技术来编写程序, 使其不阻塞当前线程,从而提高程序的响应能力。
2. 需要一种足够简单且易于理解的方式编写异步代码
在没有async/await的时代我们是如何实现异步的?下面的代码还是以Windows桌面应用为????,当click download button 时,应用发送网络请求获取project数据并show在windows窗体中。
private void DownloadButton_Click(object sender, RoutedEventArgs e)
{
...
Task<Project> task = projectService.GetAsync(id);
task.ContinueWith((t) => {
var project = t.Result;
ProjectBox.Items.Add(project);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
TaskScheduler.FromCurrentSynchronizationContext()
表示回调代码将会在当前同步上下文中执行,如果去掉此参数,可能导致程序抛出异常。原因是将回调代码安排到线程池中的非UI线程时,它无法访问UI控件。
使用回调函数的方式编写单个异步操作,似乎看不出来有什么不妥。但是随着异步操作的增加,我们就会遇到下面这样的代码。????????????
private void DownloadButton_Click(object sender, RoutedEventArgs e)
{
...
task1.ContinueWith((t) => {
...
task2.ContinueWith(t =>
{
...
task3.ContinueWith(t =>
{
...
task4.ContinueWith(t =>
{
...
});
});
});
}, TaskScheduler.FromCurrentSynchronizationContext());
}
上面的代码可能是很多人对异步望而生畏的原因,由于其功能逻辑代码嵌套的层次太多,导致可读性降低,维护困难。
对于上述被称之为回调地狱的问题,async/await提供了一种简单且易于理解的方式来编写异步操作,其代码如下所示。????????????
private async void DownloadButton_Click(object sender, RoutedEventArgs e)
{
...
Project project = await projectService.GetAsync(id);
ProjectBox.Items.Add(project);
}
Async/Await是如何实现的
下面这张图片,来自microsoft 的一篇文档 Task asynchronous programming model,它描述了在异步编程中控制流是如何在方法之间移动的。
上图很清晰的描述出了AccessTheWebAsync
方法内部代码的执行过程,那么疑问????️是:Async/Await是如何实现暂停后续代码的执行,将控制权交还给调用者,而在异步操作完成时继续执行后续代码的?
AsyncStateMachine:
为了解开疑团,我将下面的代码进行了反编译。
public async Task<ProjectVo> GetAsync(long projectId)
{
Project resultOfAwaiter1 = await projectRepo.GetAsync(projectId);
List<Person> resultOfAwaiter2 = await personRepo.GetAsync(project.getMembers());
ProjectVo result = ToProjectVo(resultOfAwaiter1, resultOfAwaiter2);
return result;
}
下面是使用dotPeek
反编译后得到的代码,为了便于理解我将得到的代码进行了一些调整。(主要是重命名和去除一些无益于理解其原理的代码)
从代码中我们可以看到在编译后async和await关键字不见了,取而代之的是:编译器为我们生成了实现IAsyncStateMachine
接口的内部类GetAsync_StateMachine
,并在GetAsync
方法体内setup并初始化它的实例。通过stateMachine.builder.Start
来启动状态机,并在最后返回一个新的task。
[AsyncStateMachine(typeof (ProjectService.GetAsync_StateMachine))]
public Task<ProjectVo> GetAsync(long projectId)
{
MainWindow.AwaitButtonClick_StateMachine stateMachine = new MainWindow.AwaitButtonClick_StateMachine();
stateMachine.caller = this;
stateMachine.projectId = projectId;
stateMachine.builder = AsyncTaskMethodBuilder<ProjectVo>.Create();
stateMachine.state = -1;
// Start方法内部执行 -> stateMachine.MoveNext()
stateMachine.builder.Start<ProjectService.GetAsync_StateMachine>(ref stateMachine);
// 返回一个新的task(可能完成也可能未完成)
return stateMachine.builder.Task;
}
[CompilerGenerated]
private sealed class GetAsync_StateMachine : IAsyncStateMachine
{
public int state;
public AsyncTaskMethodBuilder<ProjectVo> builder;
public ProjectService caller;
// 原函数的传入参数
public long projectId;
// 原函数的局部变量
private Project resultOfAwaiter1;
private List<Person> resultOfAwaiter2;
private ProjectVo result;
private TaskAwaiter<Project> awaiter1;
private TaskAwaiter<List<Person>> awaiter2;
void IAsyncStateMachine.MoveNext()
{
try
{
switch (this.state)
{
case 0:
this.state = -1;
break;
case 1:
this.state = -1;
goto label_8;
case -1:
// 开始第一个Task并获得awaiter,通过awaiter来观察Task是否完成。
this.awaiter1 = this.caller.projectRepo.GetAsync(this.projectId)
.GetAwaiter();
if (!this.awaiter1.IsCompleted)
{
this.state = 0;
// 向未完成的Task中注册continuation action;
// continuation action会在Task完成时执行;
// 等同于awaiter1.onCompleted(() => this.MoveNext());
this.builder.AwaitUnsafeOnCompleted<TaskAwaiter<Project>, ProjectService.GetAsync_StateMachine>(ref this.awaiter1, ref this);
// return(即交出控制权给GetAsync的调用者)
return;
}
break;
}
// 第一个Task完成,获取结果
this.resultOfAwaiter1 = this.awaiter1.GetResult();
// 开始第二个Task
this.awaiter2 = this.caller.personRepo
.GetAsync(resultOfAwaiter1.getMembers())
.GetAwaiter();
if (!this.awaiter2.IsCompleted)
{
this.state = 1;
// 向未完成的Task中注册continuation action
this.builder.AwaitUnsafeOnCompleted<TaskAwaiter<List<Person>>, ProjectService.GetAsync_StateMachine>(ref this.awaiter2, ref this);
// return
return;
}
label_8: // 标记,用于goto跳转
// 第二个Task完成,获取结果
this.resultOfAwaiter2 = this.awaiter2.GetResult();
this.result = this.caller.ToProjectVo(this.resultOfAwaiter1, this.resultOfAwaiter2);
}
catch (Exception ex)
{
this.state = -2;
this.builder.SetException(ex);
return;
}
this.state = -2;
// 将builder标记为completed;
// 将未完成的task标记为completed;(这里的task指GetAsync的返回值)
// set result并run continuation;
this.builder.SetResult(this.result);
}
}
值得注意的是:
-
stateMachine.builder.Start
方法内部是在调用stateMachine.MoveNext
-
stateMachine.builder.Task
返回 new task, task可能已完成也可能未完成,这取决于stateMachine是否达到了最终状态即-2 -
this.builder.AwaitUnsafeOnCompleted
===awaiter1.onCompleted(() => this.MoveNext())
===task.ContinueWith(()=> this.MoveNext())
-
this.builder.SetResult(this.result)
将未完成的task标记为completed同时设置最终结果,然后执行continuation action
关于AsyncStateMachine
的生命周期:
它以状态-1启动,每当await一个未完成的task时,其状态将转换为一个非负整数,直到task完成才被再次转换为-1,如此反复直到执行完所有代码(即执行到source code中的return)才以状态-2结束。
由上我们可以得出:
-
AsyncStateMachine
内部状态的数量=n+2,n即async
方法体内await
出现的次数。 -
AsyncStateMachine
的状态为非负整数时,它会暂停执行并交出控制权,只有当它的状态为-1时才会继续执行。 - 如果足够幸运,只调用一次
MoveNext
就可以让AsyncStateMachine
变成最终状态(-2
)。
搞清楚单个state machine的运作过程后,我们来看看多个state machine是如何协作的:
下面的序列图展示了,一款Windows桌面应用在click download button时,各个方法之间的调用过程。
其主体部分为两个state machine的交互过程,即异步方法DownloadButton_Click
和projectService.GetAsync
所生成的state machine之间的交互过程。其中在DownloadButton_Click
方法内部调用了异步方法projectService.GetAsync
,而在projectService.GetAsync
方法内部则依次调用了异步方法projectRepo.GetAsync
和personRepo.GetAsync
。
图中各元素含义如下:
-
sm
为state machine的缩写,s
为state的缩写,t
为task的缩写。 - 红绿蓝三个背景块分别表示执行过程的三个阶段,即task1未完成阶段、task1完成阶段、task3完成阶段。
- 黄色方块为对应state machine的状态。
一些没有提到的细节:
- 编译器会为标记为
async
的方法创建AsyncStateMachine
,即便在方法体内没有使用await
。 - 编译器会根据异步方法的返回值类型Task和void,为stateMachine 分别setup
AsyncTaskMethodBuilder<T>
和AsyncVoidMethodBuilder
- 对于
async
的方法内的每个局部变量和方法传入参数,编译器都在AsyncStateMachine
类中为其创建对应的属性。 - 可被
await
的对象需要有T GetAwaiter()
方法并且其返回值T
需要实现INotifyCompletion
或者ICriticalNotifyCompletion
接口、具有bool IsCompleted()
方法和TResult GetResult()
方法 - 在
stateMachine.builder.Start
方法的内部代码中,在调用stateMachine.MoveNext()
的前后位置,分别捕获、还原了ExecutionContext
和SynchronizationContext
。
不是总结的总结
什么是async/await,它为我们带来了什么?
async/await本质上是通过编译器实现的语法糖,它让我们能够轻松的写出简洁、易懂、易维护的异步代码。
学习它的原理有何益处?
- 可以学习它巧妙的设计思路????
- 避免滥用,减少代码中不必要的async/await
- 可以拓宽异步问题的调试思路,快速确定产生问题的原因
欢迎拍????指正