asyncawait关键字可以让你写出和同步代码一样简洁且结构相同的异步代码

awaiting

  • await关键字简化了附加continuation的过程。
  • 其结构体如下: ```csharp var result = await expression; statement(s);

// 它的作用相等于

var awaiter = expression.GetAwaiter(); awaiter.OnCompleted(()=> { var result = awaiter.GetResult(); statment(s); });

  1. - 例子:
  2. ```csharp
  3. class Program
  4. {
  5. static async Task Main(string[] args)
  6. {
  7. await DisplayPrimeCounts();
  8. Console.ReadKey();
  9. }
  10. static async Task DisplayPrimeCounts()
  11. {
  12. int result = await GetPrimesCountAsync(2, 1000000);
  13. Console.WriteLine(result);
  14. }
  15. static Task<int> GetPrimesCountAsync(int start, int count)
  16. {
  17. return Task.Run(() => ParallelEnumerable.Range(start, count)
  18. .Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
  19. }
  20. }

async修饰符

  • async修饰符会让编译器把await当作关键字而不是标识符(C#5 以前可能会使用await作为标识符)
  • async修饰符只能应用于方法(包括lambda表达式)
    • 该方法可以返回void、Task、Task
  • async修饰符对方法的签名或public元数据没有影响(和unsafe一样),它只会影响方法内部。
    • 在接口内使用async是没有意义的。
    • 但是,可以使用async来重载非async的方法确实合法的(只要方法签名一致)
  • 使用了async修饰符的方法就是“异步函数”

异步方法如何执行

  • 遇到await表达式,执行(正常情况下)会返回到调用者那里。
    • 就像iterator(迭代器)里面的yield return。
    • 在返回之前,运行时会附加一个continuation到await的task
      • 为保证task结束时,执行会跳回原方法,从停止的地方继续执行。
    • 如果发生故障,那么异常会被重新抛出
    • 如果一切正常,那么它的返回值就会赋给await表达式。

可以await什么?

  • 你await的表达式通常时一个task
  • 也可以满足下列条件的任意对象:
    • 有GetAwaiter方法,它返回一个waiter(实现了INotifyCompletion.OnCompleted接口)
    • 返回适当类型的GetResult方法
    • 一个bool类型的IsCompleted属性

捕获本地状态

  • await表达式最牛之处就是几乎可以出现在任何地方。
  • 特别的,在异步方法内,await表达式可以替换任何表达式。
    • 除了lock表达式和unsafe上下文。
  • 例子
    1. class Program
    2. {
    3. static async Task Main(string[] args)
    4. {
    5. await DisplayPrimeCounts();
    6. Console.ReadKey();
    7. }
    8. static async Task DisplayPrimeCounts()
    9. {
    10. for (int i = 0; i < 10; i++)
    11. {
    12. Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000));
    13. }
    14. }
    15. static Task<int> GetPrimesCountAsync(int start, int count)
    16. {
    17. return Task.Run(() => ParallelEnumerable.Range(start, count)
    18. .Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    19. }
    20. }

await之后在哪个线程上执行

  • 在await表达式之后,编译器依赖于continuation(通过awaiter模式)继续执行
  • 如果在富客户端引用的UI线程上,同步上下文会保证后续是在原线程上执行;
  • 否则,就会在task结束的线程上继续执行。

UI线程的await

  • 例子

    1. public partial class MainForm : Form
    2. {
    3. public MainForm()
    4. {
    5. InitializeComponent();
    6. }
    7. private void button_Click(object sender, EventArgs e)
    8. {
    9. Go();
    10. }
    11. async void Go()
    12. {
    13. button.Enabled = false;
    14. for (int i = 1; i < 5; i++)
    15. {
    16. txtMsg.Text += await GetPrimesCountAsync(i * 1000000, 1000000) + Environment.NewLine;
    17. }
    18. button.Enabled = true;
    19. }
    20. Task<int> GetPrimesCountAsync(int start, int count)
    21. {
    22. return Task.Run(() => ParallelEnumerable.Range(start, count)
    23. .Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    24. }
    25. }
  • 本例中,只有GetPrimesCountAsync中的代码在worker线程上运行。

  • Go中的代码会“租用”UI线程上的时间
  • 可以说:Go是消息循环中“伪并发”的执行
    • 也就是说:它和UI线程处理的其它事件是穿插执行的
    • 因为这种伪并发,唯一能发生“抢占” 时刻就是在await期间。
      • 这也就简化了线程安全,防止重新进入即可
  • 这种并发发生在调用栈较浅的地方(Task.Run调用的代码里)
  • 为了从该模型获益,真正的并发代码要避免访问共享状态或UI控件。
    1. // 伪代码
    2. 为本线程设置同步上下文
    3. while(!程序结束)
    4. {
    5. 等待消息队列中发生一些事情
    6. 发生了事情,是哪种消息?
    7. 键盘/鼠标消息 -> 触发evnet handler
    8. 用户BeginInvoke/Invoke消息 -> 执行委托
    9. }