理解async特性

可以从两方面来理解async特性:

  • 从语言特性上讲,它定义了一种行为。
  • 从编译上讲,它是一个语法糖。

1. 运行到await时,async方法的行为

1.1 休眠和恢复方法

当程序运行到await关键字时,发生了两件事:

  • 运行代码的线程将会被释放。从普通方法或同步代码的角度看,就是方法返回了。
  • 当await的Task完成时,方法将会继续运行,好像从来没有返回过一样。
    这个过程就像计算机睡眠(S4 sleep)一样,方法的当前状态被存入硬盘,然后完全退出,一点内存资源都不会占用。

    一个阻塞的方法就像是计算机休眠(S3 sleep)一样,它占用很少的资源,本质上它还在运行。

1.2 记录方法的状态

首先,async方法中的所有本地变量都会被记录下来:方法参数、定义在作用域内的任何变量、其它变量(比如循环计数)、如果不是static方法的话还有this变量。所有这些变量都被作为一个object存储在托管堆上。
其次,C#需要记录方法目前到达那个await了,可能使用一个数字来表示。
另外,类似下面的大型表达式,需要一个栈来存储子表达式的返回值:

  1. 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代码块,

会使异常难以定义。

  1. //非法代码:
  2. try
  3. {
  4. page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
  5. }
  6. catch (WebException)
  7. {
  8. page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
  9. }
  10. //替代的合法写法:
  11. bool failed = false;
  12. try
  13. {
  14. page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
  15. }
  16. catch (WebException)
  17. {
  18. failed = true;
  19. }
  20. if (failed)
  21. {
  22. page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
  23. }

2.lock代码块

lock是为了防止在同一时刻不同的线程访问同一个对象。但是因为异步代码会释放线程,然后在不确定的时间之后恢复到可能不同的线程,这样一来在await过程中维护一个lock就完全没有必要了。

  • 如果需要锁住的资源并不是必须异步的,可以在await前后显式地使用两次lock:
  1. lock (sync)
  2. {
  3. // 准备调用异步方法
  4. }
  5. int myNumber = await MethodAsync();
  6. lock (sync)
  7. {
  8. // 使用异步方法的返回值
  9. }
  • 如果确实需要在异步操作中维护一些lock,那么很不幸,这很容易造成死锁。最好考虑重新设计代码结构。

3.LINQ 查询语句

在查询语句中使用await大多数情况下是非法的。因为,LINQ会被编译器编译器编译成Lambda表达式。Lambda表达式需要被标记为async。但是编译器并不会隐式地标记Lambda表达式为async。
解决方案是,将LINQ查询语句写为等价的扩展方法调用,此时可以显示地标记Lambda为async。

  1. IEnumberable<Task<int>> tasks = myInts
  2. .Where(x => x != 9)
  3. .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方法来解释编译的过程:

  1. public async Task<int> Method1()
  2. {
  3. int foo = 6;
  4. await Task.Delay(500);
  5. return foo;
  6. }

2.1 存根方法

编译器首先会将async方法替换为一个存根方法(stub Method)。

  1. public Task<int> Mehtod()
  2. {
  3. <Method>d_0 stateMachine = new <Method>d_0()
  4. stateMachine.<>_this = this;
  5. stateMachine.<>t_builder = AsyncTaskMethodBuilder<int>.Create();
  6. stateMachine.<>_state = -1;
  7. stateMachine.t_builder.Start<<Method>d_0>(ref stateMachine);
  8. return stateMachine.<>t_builder.Task;
  9. }

存根方法的大多数工作就是初始化一个结构体的变量(<Method>d_0)。这是一个状态机。存根方法调用Start方法,然后返回一个Task。

2.2 状态机结构体

这个状态机选择使用一个struct而不是class,主要是出于性能方面的考虑(如果异步方法是同步完成的,那么就无需在堆上分配空间了)。

  1. public struct <Method>d_0
  2. {
  3. ...
  4. public int <>1_state; //标记执行到第几个await,-1表示未执行
  5. public int <foo>5_1; //保存原方法中的foo变量值
  6. public MyClass <>4_this; //实例方法,保存this变量,静态方法无此项
  7. public AsyncTaskMethodBuilder<int> <>t_builder //状态机共享逻辑的Helper,与TaskCompleteSource类似,区别是它会优化异步方法,并且是一个struct不是class
  8. private object <>t_stack //用于大型表达式中的await。
  9. private TaskAwaiter <>u_$awaiter2; //临时存储,Task完成时帮助通知完成。
  10. ...
  11. }

2.3 MoveNext方法

MoveNext方法是状态机必须的方法,它在第一次运行时和从await继续运行时被调用。该方法需要进行下面的编译步骤:

1.将原方法拷贝到MoveNext方法:

  1. <foo>5_1 = 3;
  2. Task t = Task.Delay(500);
  3. //await继续的逻辑代码
  4. return <foo>5_1;

2.转换完成时的返回值

源代码中的每一个返回语句都需要转换。

  1. <>t_builder.SetResult(<foo>5_1);//设置值
  2. return; //MoveNext返回void

3.跳转到正确的位置

生成的中间代码类似下面的switch语句:

  1. switch(<>1_state)
  2. {
  3. case -1: //第一次调用时
  4. <foo>5_1 = 3;
  5. Task t = Task.Delay(500);
  6. //await继续的逻辑代码
  7. case 0: //第一个await
  8. <>t_builder.SetResult(<foo>5_1);
  9. return;
  10. }

4.运行到await时暂停方法

在Task完成时,需要更新状态。

  1. switch(<>1_state)
  2. {
  3. case -1: //第一次调用时
  4. <foo>5_1 = 3;
  5. //**************
  6. u_&awaiter2 = Task.Delay(500).GetAwaiter();
  7. //await继续的逻辑代码
  8. <>1_state = 0;
  9. <>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this);
  10. return;
  11. //**************
  12. case 0: //第一个await
  13. <>t_builder.SetResult(<foo>5_1);
  14. return;
  15. }

这个过程中还包括更复杂的过程,比如获取同步上下文等等。

5.await之后继续运行

  1. switch(<>1_state)
  2. {
  3. case -1: //第一次调用时
  4. <foo>5_1 = 3;
  5. u_&awaiter2 = Task.Delay(500).GetAwaiter();
  6. //await继续的逻辑代码
  7. <>1_state = 0;
  8. <>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this);
  9. return;
  10. case 0: //第一个await
  11. //**************
  12. <>u_$awaiter2.GetResult(); //await返回后,获取返回值
  13. //**************
  14. <>t_builder.SetResult(<foo>5_1);
  15. return;
  16. }

6.同步完成

如果await之前,Task已经完成运行,那么无需暂停,直接goto:

  1. switch(<>1_state)
  2. {
  3. case -1: //第一次调用时
  4. <foo>5_1 = 3;
  5. //Task t = Task.Delay(500);
  6. u_&awaiter2 = Task.Delay(500).GetAwaiter();
  7. //如果同步执行,直接goto,无需站厅代码
  8. if(<>u_$awaiter2.IsCompleted)
  9. {
  10. goto case 0;
  11. }
  12. <>1_state = 0;
  13. <>t_builder.AwaitUnsafeOnCompleted(<>u_$awaiter2, this);
  14. return;
  15. case 0: //第一个await
  16. <>u_$awaiter2.GetResult(); //await返回后,获取返回值
  17. <>t_builder.SetResult(<foo>5_1);
  18. return;
  19. }

7.捕获异常

如果在async方法运行期间抛出了异常,但是没有try…catch代码来处理异常,编译器生成的代码会捕获这个异常,然后设置返回的Task为faulted。





posted @ 2016-09-06 17:28  qianzi  阅读(939)  评论(0编辑  收藏  举报