理解async/await异步模型(一)

大概从 Unity 2017 开始就正式支持 .Net 4.x 版本,很多新的语法都支持了,包括 dynamic 动态类型、async/await 异步模型,其中对 async/await 的支持尤其重要,虽然之前可以使用协程来进行异步编程,但是协程存在很多限制,最突出的两个问题是,没有返回值,以及异常处理问题,async/await的出现,很好的解决的这些问题,但是同时又带了一些新的问题。

async/await异步模型使用起来很简单,但是要真正的完全理解,其中涉及到很多难理解的概念,比如多线程、线程池、同步上下文、任务调度等等,对于初学者,这些单个概念都不太好理解,MSDN 上面提供的资料也不是很多,同时,本人的能力水平有限,其中难免存在纰漏,但是我还是尽可能的将我的理解表述清楚,便于读者理解。

下面文章从一个简单的例子出发,首先了解一下 async/await 是如何实现异步的。

async/await是怎么工作的

在 C# 中,async/await语法的作用其实只是告诉编译器,它所修饰的方法是一个异步方法,需要编译器进行相应的转化,还是直接看一个简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Program
{
static void Main(string[] args)
{
AsyncSample sample = new AsyncSample();
sample.DoAsync();

Console.ReadKey();
}
}

public class AsyncSample
{
public async Task DoAsync()
{
Console.WriteLine("Before: {0}", Thread.CurrentThread.ManagedThreadId);

await Task.Delay(1000);

Console.WriteLine("After: {0}", Thread.CurrentThread.ManagedThreadId);
}
}

运行结果如下:

1
2
Before: 1
After: 4

上面代码比较简单,AsyncSample 类里面包含一个 DoAsync() 方法,可以看到 await 语句前后代码块不在同一个线程内执行,这就很有意思了,为了研究它的原理,先看一下它的反编译结果,我这里使用的是 ILSpy,下面结果是对反编译字段进行重新命名的,另外反编译的时候选择 C# 4.0 语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
internal class Program
{
private static void Main(string[] args)
{
AsyncSample sample = new AsyncSample();
sample.DoAsync();
Console.ReadKey();
}
}

public class AsyncSample
{
[CompilerGenerated]
private sealed class DOAsyncStateMechine : IAsyncStateMachine
{
public int state;

public AsyncTaskMethodBuilder builder;

public AsyncSample inst;

private TaskAwaiter awaiter;

private void MoveNext()
{
int num = state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
Console.WriteLine("Before: {0}", Thread.CurrentThread.ManagedThreadId);
awaiter = Task.Delay(1000).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (state = 0);
awaiter = awaiter;
DOAsyncStateMechine stateMachine = this;
builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = awaiter;
awaiter = default(TaskAwaiter);
num = (state = -1);
}
awaiter.GetResult();
Console.WriteLine("After: {0}", Thread.CurrentThread.ManagedThreadId);
}
catch (Exception exception)
{
state = -2;
builder.SetException(exception);
return;
}
state = -2;
builder.SetResult();
}

void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}

[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}

void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}

[AsyncStateMachine(typeof(DOAsyncStateMechine))]
[DebuggerStepThrough]
public Task DoAsync()
{
DOAsyncStateMechine stateMachine = new DOAsyncStateMechine();
stateMachine.inst = this;
stateMachine.builder = AsyncTaskMethodBuilder.Create();
stateMachine.state = -1;
AsyncTaskMethodBuilder builder = stateMachine.builder;
builder.Start(ref stateMachine);
return stateMachine.builder.Task;
}
}

对比反编译结果和源码,可以法线变化的只是 AsyncSample 类,变化主要有三点:

  • DoAsync()方法去掉了 async 修饰;
  • DoAsync()方法实现完全被重写;
  • 增加了 DOAsyncStateMechine 嵌套类,这是一个状态机类型,状态机的主体是 MoveNext(),因为源码里只有一个await,所以这里有三个状态,如下图所示:
digraph finite_state_machine { rankdir=LR; size="8,5" entry [shape=circle label="入口"] stage1[label="Before"] stage2[label="Delay"] stage3[label="After"] exit [shape= circle label="结束"] entry -> stage1 [label="state != 0"] entry -> stage3 [label = "state == 0"] stage1 -> stage2 [label="执行任务"]; stage2 -> stage3 [label="awaiter.IsCompleted == true"]; stage3 -> exit; stage2 -> exit[label="awaiter.IsCompleted == false"]; }

下面梳理一下状态机执行流程,从 DoAsync() 开始:

  1. 首先构建状态机,初始 builder 成员,并将状态机的初始状态设置为 -1
  2. 然后调用 builder.Start(ref stateMachine) 该方法会调用状态机的 MoveNext() 函数;
  3. MoveNext() 中,由于初始 state == -1 因此进入第一阶段,执行Before 部分代码;
  4. 然后紧接着进入阶段二,执行 Task.Delay(1000),该方法返回一个 Task 类型对象,调用 Task.GetAwaiter() 获取 TaskAwaiter 对象,然后通过IsCompleted属性判断任务是否完成,如果完成,则直接跳转到阶段三;如果没有完成,则会将 state设置为0,然后调用builder.builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)并退出状态机,它会在任务结束之后,再次调用状态机;
  5. 任务完成后,再次调用状态机的 MoveNext(),此时直接进入到阶段三,至此整个状态机执行完毕。

通过上面的过程,大致的了解了编译器对 async/await 代码做了什么,能够知道为什么一个 await 语句就能够做到异步执行,但是这里还没有解决开头提出的问题,为什么 await 语句前后执行线程不同? TaskAwaiter 内部是如何工作的? 以及 builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine) 内部是如何实现的?

这里为了方便理解,采用了Task.Delay来进行演示,如果换成Task.Run(Action)又会稍微复杂一点,为了研究明白这些问题,后序将会按照以下两大块来分别研究:

  • 任务是如何异步的执行的?
  • 任务是如何检测结束的?
  • 任务结束之后,是如何执行后续任务的?

总结

通过以上的过程,能够初步了解 async/await是怎么一个工作机制,就是通过状态机定义一系列的状态转换,awaiter 语句前后代码安排在特定的状态执行。