1.1 休眠和恢复方法
1.2 记录方法的状态
1.3 获取上下文
1.4 await不能使用的情况
1.catch和finally代码块,
2.lock代码块
3.LINQ 查询语句
4.unsafe代码块
1.5 async方法只有在需要时才是异步的
2. async的编译过程
2.1 存根方法
2.2 状态机结构体
2.3 MoveNext方法
1.将原方法拷贝到MoveNext方法:
2.转换完成时的返回值
3.跳转到正确的位置
4.运行到await时暂停方法
5.await之后继续运行
6.同步完成
7.捕获异常
可以从两方面来理解async特性:
- 从语言特性上讲,它定义了一种行为。
- 从编译上讲,它是一个语法糖。
1. 运行到await时,async方法的行为
1.1 休眠和恢复方法
当程序运行到await关键字时,发生了两件事:
- 运行代码的线程将会被释放。从普通方法或同步代码的角度看,就是方法返回了。
- 当await的Task完成时,方法将会继续运行,好像从来没有返回过一样。
这个过程就像计算机睡眠(S4 sleep)一样,方法的当前状态被存入硬盘,然后完全退出,一点内存资源都不会占用。一个阻塞的方法就像是计算机休眠(S3 sleep)一样,它占用很少的资源,本质上它还在运行。
1.2 记录方法的状态
首先,async方法中的所有本地变量都会被记录下来:方法参数、定义在作用域内的任何变量、其它变量(比如循环计数)、如果不是static方法的话还有this变量。所有这些变量都被作为一个object存储在托管堆上。
其次,C#需要记录方法目前到达那个await了,可能使用一个数字来表示。
另外,类似下面的大型表达式,需要一个栈来存储子表达式的返回值:
int myNum = await MethodAsync(await myTask, await Method2Async());
最后,await表达式返回的Task也需要存储。
1.3 获取上下文
C#会在await时获取各种上下文,并且在方法继续时恢复上下文。
最重要的上下文是同步上下文(synchronization context),对于UI应用程序尤其重要。
调用上下文(CallContext),存储逻辑线程生命周期内的数据。使用在程序中使用这个上下文是一个糟糕的实践,虽然它可以减少方法的参数。这个上下文在异步环境下没有用,因为方法可能在一个完全不同的线程上恢复。
1.4 await不能使用的情况
await可以在标记为async的方法的大多数位置使用,但是有一些例外:
1.catch和finally代码块,
会使异常难以定义。
//非法代码:
try
{
page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
}
catch (WebException)
{
page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
}
//替代的合法写法:
bool failed = false;
try
{
page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
}
catch (WebException)
{
failed = true;
}
if (failed)
{
page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
}
2.lock代码块
lock是为了防止在同一时刻不同的线程访问同一个对象。但是因为异步代码会释放线程,然后在不确定的时间之后恢复到可能不同的线程,这样一来在await过程中维护一个lock就完全没有必要了。
- 如果需要锁住的资源并不是必须异步的,可以在await前后显式地使用两次lock:
lock (sync)
{
// 准备调用异步方法
}
int myNumber = await MethodAsync();
lock (sync)
{
// 使用异步方法的返回值
}
- 如果确实需要在异步操作中维护一些lock,那么很不幸,这很容易造成死锁。最好考虑重新设计代码结构。
3.LINQ 查询语句
在查询语句中使用await大多数情况下是非法的。因为,LINQ会被编译器编译器编译成Lambda表达式。Lambda表达式需要被标记为async。但是编译器并不会隐式地标记Lambda表达式为async。
解决方案是,将LINQ查询语句写为等价的扩展方法调用,此时可以显示地标记Lambda为async。
IEnumberable<Task<int>> tasks = myInts
.Where(x => x != 9)
.Select(async x => await DoSomethingAsync(x) + await DoSomethingElseAsync(x));
4.unsafe代码块
unsafe代码应该保持独立,它不需要是异步的。await关键的编译会破坏unsaf代码。
1.5 async方法只有在需要时才是异步的
async方法在到达第一个await才会暂停,但是这不是一定的。有时在到达第一个await时,task已经执行完成了。下面情况下,Task可能已经完成:
- 创建时就完成了。
- 从一个没有执行await的async方法返回的Task。
- 异步操作确实已经完成了。(可能因为在await之前,线程忙于其它工作)
- 从一个执行到await的async方法返回,但是这个方法中await的Task也已经完成。(此时,整个异步方法链是同步的)
2. async的编译过程
下面从一个简单的async方法来解释编译的过程:
public async Task<int> Method1()
{
int foo = 6;
await Task.Delay(500);
return foo;
}
2.1 存根方法
编译器首先会将async方法替换为一个存根方法(stub Method)。
public Task<int> Mehtod()
{
<Method>d_0 stateMachine = new <Method>d_0()
stateMachine.<>_this = this;
stateMachine.<>t_builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>_state = -1;
stateMachine.t_builder.Start<<Method>d_0>(ref stateMachine);
return stateMachine.<>t_builder.Task;
}
存根方法的大多数工作就是初始化一个结构体的变量(<Method>d_0
)。这是一个状态机。存根方法调用Start方法,然后返回一个Task。
2.2 状态机结构体
这个状态机选择使用一个struct而不是class,主要是出于性能方面的考虑(如果异步方法是同步完成的,那么就无需在堆上分配空间了)。
public struct <Method>d_0
{
...
public int <>1_state; //标记执行到第几个await,-1表示未执行
public int <foo>5_1; //保存原方法中的foo变量值
public MyClass <>4_this; //实例方法,保存this变量,静态方法无此项
public AsyncTaskMethodBuilder<int> <>t_builder; //状态机共享逻辑的Helper,与TaskCompleteSource类似,区别是它会优化异步方法,并且是一个struct不是class
private object <>t_stack; //用于大型表达式中的await。
private TaskAwaiter <>u_$awaiter2; //临时存储,Task完成时帮助通知完成。
...
}
2.3 MoveNext方法
MoveNext方法是状态机必须的方法,它在第一次运行时和从await继续运行时被调用。该方法需要进行下面的编译步骤:
1.将原方法拷贝到MoveNext方法:
<foo>5_1 = 3;
Task t = Task.Delay(500);
//await继续的逻辑代码
return <foo>5_1;
2.转换完成时的返回值
源代码中的每一个返回语句都需要转换。
<>t_builder.SetResult(<foo>5_1);//设置值
return; //MoveNext返回void
3.跳转到正确的位置
生成的中间代码类似下面的switch语句:
switch(<>1_state)
{
case -1: //第一次调用时
<foo>5_1 = 3;
Task t = Task.Delay(500);
//await继续的逻辑代码
case 0: //第一个await
<>t_builder.SetResult(<foo>5_1);
return;
}
4.运行到await时暂停方法
在Task完成时,需要更新状态。
switch(<>1_state)
{
case -1: //第一次调用时
<foo>5_1 = 3;
//**************
u_&awaiter2 = Task.Delay(500).GetAwaiter();
//await继续的逻辑代码
<>1_state = 0;
<>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this);
return;
//**************
case 0: //第一个await
<>t_builder.SetResult(<foo>5_1);
return;
}
这个过程中还包括更复杂的过程,比如获取同步上下文等等。
5.await之后继续运行
switch(<>1_state)
{
case -1: //第一次调用时
<foo>5_1 = 3;
u_&awaiter2 = Task.Delay(500).GetAwaiter();
//await继续的逻辑代码
<>1_state = 0;
<>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this);
return;
case 0: //第一个await
//**************
<>u_$awaiter2.GetResult(); //await返回后,获取返回值
//**************
<>t_builder.SetResult(<foo>5_1);
return;
}
6.同步完成
如果await之前,Task已经完成运行,那么无需暂停,直接goto:
switch(<>1_state)
{
case -1: //第一次调用时
<foo>5_1 = 3;
//Task t = Task.Delay(500);
u_&awaiter2 = Task.Delay(500).GetAwaiter();
//如果同步执行,直接goto,无需站厅代码
if(<>u_$awaiter2.IsCompleted)
{
goto case 0;
}
<>1_state = 0;
<>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this);
return;
case 0: //第一个await
<>u_$awaiter2.GetResult(); //await返回后,获取返回值
<>t_builder.SetResult(<foo>5_1);
return;
}
7.捕获异常
如果在async方法运行期间抛出了异常,但是没有try…catch代码来处理异常,编译器生成的代码会捕获这个异常,然后设置返回的Task为faulted。