什么是异步

启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序的资源的集合。这些资源包括虚地址空间文件句柄和许多其他程序运行所需的东西。
在进程内部,系统创建了一个称为线程的内核(kernel)对象,它代表了真正执行的程序。一旦进程建立,系统会在Main方法的第一行语句处就开始线程的执行。
有关线程的知识

  • 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束。
  • 线程可以派生其他线程,因此在任何时刻,一个进程都可能包含不同状态的多个线程,来执行程序的不同部分
  • 如果一个进程有多个线程,它们将共享进程的资源
  • 系统为处理器执行所规划的单元是线程

在异步程序中,程序代码不需要按照编写时的顺序严格执行。C#5.0引入了构建异步方法的async和await关键字

使用异步和不使用异步的区别

例:不使用异步的示例。

  1. using System;
  2. using System.Diagnostics;
  3. using System.Net;
  4. public class MyDownloadString
  5. {
  6. Stopwatch sw = new Stopwatch();
  7. public void DoRun()
  8. {
  9. const int LargeNumber = 6_000_000;
  10. sw.Start();
  11. int t1 = CountCharacters(1, "http://www.microsoft.com");
  12. int t2 = CountCharacters(2, "http://www.illustratedcsharp.com");
  13. CountToALargeNumber(1, LargeNumber);
  14. CountToALargeNumber(2, LargeNumber);
  15. CountToALargeNumber(3, LargeNumber);
  16. CountToALargeNumber(4, LargeNumber);
  17. Console.WriteLine("Chars in http://www.microsoft.com : {0}", t1);
  18. Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2);
  19. }
  20. /// <summary>
  21. /// 下载某网站的内容,返回该网站包含的字符数
  22. /// </summary>
  23. /// <param name="id"></param>
  24. /// <param name="uriString"></param>
  25. /// <returns></returns>
  26. private int CountCharacters(int id, string uriString)
  27. {
  28. WebClient wc1 = new WebClient();
  29. Console.WriteLine("Starting call {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
  30. string result = wc1.DownloadString(new Uri(uriString));
  31. Console.WriteLine(" Call {0} completed: {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
  32. return result.Length;
  33. }
  34. /// <summary>
  35. /// 执行一个消耗一定时间的任务
  36. /// </summary>
  37. /// <param name="id"></param>
  38. /// <param name="value"></param>
  39. private void CountToALargeNumber(int id, int value)
  40. {
  41. for (long i = 0; i < value; i++) ;
  42. Console.WriteLine(" End counting {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
  43. }
  44. }
  45. class Program
  46. {
  47. static void Main(string[] args)
  48. {
  49. MyDownloadString ds = new MyDownloadString();
  50. ds.DoRun();
  51. Console.ReadKey();
  52. }
  53. }

输出结果如下所示,计时以毫秒为单位。每次运行的结果可能不同:
C#图解教程之异步编程 - 图1
下图总结了输出结果。Call1和Call2占用了大部分时间,而且都浪费在等待网站的响应上。
C#图解教程之异步编程 - 图2
如果我们能初始化两个CountCharacter调用,无需等待结果,而是直接执行4个CountToALargeNumber调用,然后在两个CountCharacter方法调用结束时再获取结果,就可以显著地提高性能。这里可以使用C#的async/await特性

  • 当DoRun调用CountCharactersAsync时,CountCharactersAsync将立即返回,然后才真正开始下载字符。它向调用方法返回的是一个Task类型的占位符对象,表示它计划进行的工作。这个占位符最终将“返回”一个int
  • 这使得DoRun不用等待实际工作完成就可以继续执行。下一条语句是再次调用CountCharactersAsync,同样会返回一个Task对象
  • 接着,DoRun可以继续执行,调用4次CountToALargeNumber,同时CountCharactersAsync的两次调用继续它们的工作——基本上是等待。
  • DoRun的最后两行CountCharactersAsync调用返回的Tasks中获取结果。如果还没有结果,将阻塞并等待。

例:使用异步示例。

  1. using System;
  2. using System.Diagnostics;
  3. using System.Net;
  4. using System.Threading.Tasks;
  5. public class MyDownloadString
  6. {
  7. Stopwatch sw = new Stopwatch();
  8. public void DoRun()
  9. {
  10. const int LargeNumber = 6_000_000;
  11. sw.Start();
  12. Task<int> t1 = CountCharactersAsync(1, "http://www.microsoft.com"); // 保存结果的对象
  13. Task<int> t2 = CountCharactersAsync(2, "http://www.illustratedcsharp.com"); // 保存结果的对象
  14. CountToALargeNumber(1, LargeNumber);
  15. CountToALargeNumber(2, LargeNumber);
  16. CountToALargeNumber(3, LargeNumber);
  17. CountToALargeNumber(4, LargeNumber);
  18. Console.WriteLine("Chars in http://www.microsoft.com : {0}", t1.Result); // 获取结果
  19. Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2.Result); // 获取结果
  20. }
  21. private async Task<int> CountCharactersAsync(int id, string site) // Task<int>表示正在执行的工作,最终将返回int
  22. {
  23. WebClient wc = new WebClient();
  24. Console.WriteLine("Starting call {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
  25. string result = await wc.DownloadStringTaskAsync(new Uri(site)); // 该操作需要等待
  26. Console.WriteLine(" Call {0} completed: {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
  27. return result.Length;
  28. }
  29. private void CountToALargeNumber(int id, int value)
  30. {
  31. for (long i = 0; i < value; i++) ;
  32. Console.WriteLine(" End counting {0} : {1, 4:N0} ms", id, sw.Elapsed.TotalMilliseconds);
  33. }
  34. }

输出结果:
C#图解教程之异步编程 - 图3
下图总结了输出结果,展示了修改后的程序的时间轴。新版程序比旧版快了32%!这是因为CountToALargeNumber的4次调用是在CountCharactersAsync方法调用等待网站响应的时候进行的
C#图解教程之异步编程 - 图4

async/await特性的结构

如果一个程序调用某个方法,等待其执行所有处理后才继续执行,我们就称这一的方法是同步的,这是默认的
相反,异步的方法在处理完成之前就返回到调用方法。
C#的async/await特性可以创建并使用异步方法。该特性由三个部分组成:

  • 调用方法(calling method):该方法调用异步方法,然后在异步方法执行其任务的时候继续执行。
  • 异步(async)方法:该方法异步执行其工作,然后立即返回到调用方法。
  • await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告。

    异步方法的语法

  1. static class DoAsyncStuff
  2. {
  3. // 异步方法
  4. public static async Task<int> CalculateSumAsync(int i1, int i2)
  5. {
  6. int sum = await TaskEx.Run(() = > GetSum(i1, i2)); // await表达式
  7. return sum;
  8. }
  9. }
  10. class Program
  11. {
  12. // 调用方法
  13. static void Main()
  14. {
  15. Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6);
  16. }
  17. }

什么是异步方法

异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。
异步方法语法特点

  • 方法头中包含async方法修饰符
  • 包含一个或多个await表达式,表示可以异步完成的任务。
  • 必须具有三种返回类型,voidTaskTask。Task和Task的返回对象表示将在未来完成的工作。
  • 异步方法的参数可以为任何类型任意数量,但不能为out或ref参数
  • 异步方法的名称应该以Async为后缀命名
  • 除了方法以外,Lambda表达式和匿名方法也可以作为异步对象

例:异步方法示例。

  1. 关键字 返回类型
  2. async Task<int> CountCharactersAsync(int id, string site)
  3. {
  4. Console.WriteLine("Starting CountCharacters");
  5. WebClient wc = new WebClient();
  6. string result = await wc.DownloadStringTaskAsync(new Uri(site)); // await表达式
  7. Console.WriteLine("CountCharacters Completed");
  8. return result.Length; // 返回语句
  9. }

异步方法的组成

  • 异步方法在方法头中必须包含async关键字,而且必须出现在返回类型之前
  • 该修饰符只是标识该方法包含一个或多个await表达式。本身并不能创建任何异步操作。
  • async关键字是一个上下文关键字,即除了作为方法修饰符之外,async还可用作标识符

三种返回类型
Task:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task。调用方法将通过读取Task的Result属性来获取这个T类型的值。

  1. Task<int> value = DoStuff.CalculateSumAsync(5, 6);
  2. Console.WriteLine("Value: {0}", value.Result);

Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个Task类型的对象。这时,即使异步方法中出现了return语句也不会返回任何东西

  1. Task someTask = DoStuff.CalculateSumAsync(5, 6);
  2. someTask.Wait();

void:如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时调用并忘记[fire and forget]),异步方法可以返回void类型。这时,即使异步方法中出现return语句也不会返回任何东西
注意:任何返回Task类型的异步方法,其返回值必须为T类型或可以隐式转换为T的类型。
例:使用返回Task对象的异步方法。

  1. using System;
  2. using System.Threading.Tasks;
  3. class Program
  4. {
  5. static void Main()
  6. {
  7. Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6);
  8. // 处理其他事情
  9. Console.WriteLine("Value: {0}", value.Result);
  10. }
  11. }
  12. static class DoAsyncStuff
  13. {
  14. public static async Task<int> CalculateSumAsync(int i1, int i2)
  15. {
  16. int sum = await Task.Run(() => GetSum(i1, i2));
  17. return sum;
  18. }
  19. private static int GetSum(int i1, int i2)
  20. {
  21. return i1 + i2;
  22. }
  23. }

例:使用返回Task对象的异步方法。

  1. using System;
  2. using System.Threading.Tasks;
  3. class Program
  4. {
  5. static void Main()
  6. {
  7. Task someTask = DoAsyncStuff.CalculateSumAsync(5, 6);
  8. // 处理其他事情
  9. someTask.wait();
  10. Console.WriteLine("Async stuff is done");
  11. }
  12. }
  13. static class DoAsyncStuff
  14. {
  15. public static async Task CalculateSumAsync(int i1, int i2)
  16. {
  17. int value = await Task.Run(() => GetSum(i1, i2));
  18. Console.WriteLine("Value: {0}", value);
  19. }
  20. private static int GetSum(int i1, int i2)
  21. {
  22. return i1 + i2;
  23. }
  24. }

例:使用“调用并忘记”的异步方法。

  1. using System;
  2. using System.Threading;
  3. using System.Threading.Tasks;
  4. class Program
  5. {
  6. static void Main()
  7. {
  8. DoAsyncStuff.CalculateSumAsync(5, 6);
  9. // 处理其他事情
  10. Thread.Sleep(200); // 由于使用了Thread.Sleep方法来暂停当前线程,所以异步方法完成时它还没完成
  11. Console.WriteLine("Program Exiting");
  12. }
  13. }
  14. static class DoAsyncStuff
  15. {
  16. public static async void CalculateSumAsync(int i1, int i2)
  17. {
  18. int value = await Task.Run(() => GetSum(i1, i2));
  19. Console.WriteLine("Value: {0}", value);
  20. }
  21. private static int GetSum(int i1, int i2)
  22. {
  23. return i1 + i2;
  24. }
  25. }

异步方法的控制流

异步方法的结构包含三个不同的区域

  • await表达式之前的部分:从方法开头到第一个await表达式之间的所有代码。这部分应该只包含少量且无需长时间处理的代码。
  • await表达式:表示将被异步执行的任务。
  • 后续部分:在await表达式之后出现的方法中的其余代码。包括其执行环境,如所在线程信息、目前作用域内的变量值,以及当await表达式完成后要重新执行所需的其他信息。

C#图解教程之异步编程 - 图5
注意:当达到await表达式时,异步方法将控制返回到调用方法。如果方法的返回类型为Task或Task类型,将创建一个Task对象,表示需异步完成的任务和后续,然后将该Task返回到调用方法
目前有两个控制流异步方法内的调用方法内的。异步方法内的代码完成以下工作:

  • 异步执行await表达式的空闲任务。
  • 当await表达式完成时,执行后续部分。
  • 当后续部分遇到return语句或达到方法末尾时,根据返回类型void、Task、Task设置对应的属性并退出控制流

同时,调用方法中的代码将继续其进程从异步方法获取Task对象。当需要其实际值时,就引用Task对象的Result属性。届时,如果异步方法设置了该属性,调用方法就能获得该值并继续。否则,将暂停并等待该属性被设置,然后再继续执行
C#图解教程之异步编程 - 图6

await表达式

await表达式指定了一个异步执行的任务。
语法:由await关键字和一个空闲对象(称为任务)组成。
这个任务可能是一个Task类型的对象,也可能不是。默认情况下,这个任务在当前线程异步运行。

  1. await task

一个空闲对象即是一个awaitable类型的实例。awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象。awaiter类型包含以下成员。

  • bool isCompleted { get; }
  • void OnCompleted(Action);

它还包含以下成员之一:

  • void GetResult();
  • T GetResult(); // T为任意类型

然而实际上,并不需要构建awaitable。应该使用Task类,它是awaitable类型。

Task.Run方法

尽管目前BCL中存在许多返回Task类型对象的方法,但是也可以编写自己的方法
最简单的方式是在方法中使用Task.Run方法来创建一个Task。Task.Run是在不同的线程上运行方法的
例:Task.Run方法接受一个Func委托作为参数。

  1. class MyClass
  2. {
  3. public int Get10() // 与Func<int>兼容
  4. {
  5. return 10;
  6. }
  7. public async Task DoWorkAsync()
  8. {
  9. Func<int> ten = new Func<int>(Get10);
  10. int a = await Task.Run(ten);
  11. int b = await Task.Run(new Func<int>(Get10)); // 在参数列表创建委托
  12. int c = await Task.Run(() => { return 10; }); // Lambda表达式隐式转换为Func<int>委托
  13. Console.WriteLine("{0} {1} {2}", a, b, c);
  14. }
  15. }
  16. class Program
  17. {
  18. static void Main()
  19. {
  20. Task t = (new MyClass()).DoWorkAsync();
  21. t.Wait(); // 10 10 10
  22. }
  23. }

上面的代码中,使用的Task.Run的签名Func为参数。该方法共8个重载如下所示:
C#图解教程之异步编程 - 图7
可能用到的4个委托类型的签名:
C#图解教程之异步编程 - 图8
例:使用Task.Run方法来运行4种不同的委托类型。

  1. static class MyClass
  2. {
  3. public static async Task DoWorkAsync()
  4. {
  5. await Task.Run(() => Console.WriteLine(5.ToString())); // Action
  6. Console.WriteLine((await Task.Run(() => 6)).ToString()); // TResult Func()
  7. await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString()))); // Task Func()
  8. int value = await Task.Run(() => Task.Run(() => 8)); // Task<TResult> Func()
  9. Console.WriteLine(value.ToString());
  10. }
  11. }
  12. class Program
  13. {
  14. static void Main()
  15. {
  16. Task t = MyClass.DoWorkAsync();
  17. t.Wait(); // 5 6 7 8
  18. }
  19. }

任何可以使用表达式的地方,都可以使用await表达式,只要位于异步方法内。上面的代码中:

  • 第一个和第三个实例将await表达式用作语句
  • 第二个实例将await表达式用作WriteLine方法的参数
  • 第四个实例将await表达式用作赋值语句的右端

例:用可接受的Func委托的形式创建一个Lambda函数。

  1. static class MyClass
  2. {
  3. private static int GetSum(int i1, int i2)
  4. {
  5. return i1 + i2;
  6. }
  7. public static async Task DoWorkAsync()
  8. {
  9. int value = await Task.Run(() => GetSum(5, 6)); // TResult Func()
  10. Console.WriteLine(value.ToString());
  11. }
  12. }
  13. class Program
  14. {
  15. static void Main()
  16. {
  17. Task t = MyClass.DoWorkAsync();
  18. t.Wait(); // 11
  19. }
  20. }

原文地址:https://zhdaa.github.io/2019/09/01/C-图解教程之异步编程