zoukankan      html  css  js  c++  java
  • 理解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。





  • 相关阅读:
    Systemd 进程管理器
    Fedora 15 LoveLock的新特性
    fedora 15 iso 硬盘安装
    Linux权限360度赤裸裸华丽丽大曝光连载之二:SetUID
    Linux下socket设置为非阻塞方式和fcntl系统调用
    linux 磁盘 空间 不足 符号链接
    U盘成功安装REHL 6.1
    IT公司中最流行的10种编程语言
    C会否像汇编一样退居幕后?
    白宫决策捕杀拉登现场照片公布
  • 原文地址:https://www.cnblogs.com/qianzi067/p/5846530.html
Copyright © 2011-2022 走看看