• 异步和多线程不是同一个概念,异步相当于餐厅服务员给你递给菜单,然后剩下的你自己看菜单,看完再叫服务员(服务员只负责给客人递菜单,递完一个到下一个那边),而多线程相当于n个服务员对应n个客人,一对一服务。
  • 异步方法具有传染性,await会阻塞当前异步线程,直到异步方法执行完毕才开始执行后续的代码逻辑。
  • 不写async的方法 有返回值:异步方法().Result 无需写await了手动取值无返回值:异步方法().Wait() 容易造成死锁,谨慎使用
  • 异步Lambda表达式 在obj前面加上async,在表达式前面加上async
  • await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码。
  • 如果一个异步方法只是对别的异步方法的调用的转发,并没有太多复杂的逻辑(比如等待A的结果,再调用B;把A调用的返回值拿到内部做一些处理再返回),那么就可以去掉async关键字
  • 利用await Task.Delay(TimeSpan.FromSeconds(2))来替代Thread.Sleep()
  • 在ASP.NET开发中,只要有cancellationtoken参数就传入。Action方法都传入CancellationToken token参数,调用异步方法能传token都传这个参数。
  • Task.WhenAll Task.WhenAny
  • yield return和异步 async IAsyncEnumerable返回值 MethodName() 调用的时候await foreach

    一 基础概念

    异步方法”:用async关键字修饰
    1.返回值一般是Task T是真正的返回值类型,异步方法名字以Aysnc结尾
    2.即使方法没有返回值,也最好把返回值申明为非泛型的Task
    3.调用泛型方法时,一般在方法前加上await,这样拿到的返回值就是泛型指定的T类型。
    4.异步方法具有传染性:一个方法中如果有await调用,则这个方法必须修饰为async。

主函数Main里面调用了异步方法,void必须更改为Task,如下:
image.png

await可以将返回值从Task中取出来

  1. static async Task Main(string[] args)
  2. {
  3. //string fileName = @"E:\albert.txt";
  4. //File.WriteAllText(fileName, "Hello");
  5. //string s = File.ReadAllText(fileName);
  6. //Console.WriteLine(s);
  7. string fileName = @"E:\albert.txt";
  8. await File.WriteAllTextAsync(fileName, "Albert");
  9. //这里面加了await把Task<string>中的返回值取出来了
  10. /*
  11. * 第二种写法:
  12. Task<string> t = File.ReadAllTextAsync(fileName);
  13. string s = await t;
  14. *
  15. */
  16. string s = await File.ReadAllTextAsync(fileName);
  17. Console.WriteLine(s);
  18. Console.ReadLine();
  19. }

解决线程冲突,等待当前方法执行完再执行下面的方法。

二 异步方法(Self Create)

HttpClient最好和HttpFactory一起使用
HttpClient实现了IDisposable所以需要写using进行回收

  1. static async Task Main(string[] args){
  2. string filename = @"E:\albert.txt";
  3. await DownloadHtmlAysnc("http://www.baidu.com",filename);
  4. }
  5. /// <summary>
  6. /// 从某个网站下载html到文件中
  7. /// </summary>
  8. /// <param name="url"></param>
  9. /// <param name="filename"></param>
  10. /// <returns></returns>
  11. static async Task DownloadHtmlAysnc(string url,string filename)
  12. {
  13. //最好和HttpFactory一起使用
  14. //HttpClient实现了IDisposable所以需要写using进行回收
  15. using (HttpClient httpClient = new HttpClient())
  16. {
  17. string html = await httpClient.GetStringAsync(url);
  18. await File.WriteAllTextAsync(filename, html);
  19. }
  20. }

三 如果调用方不支持异步方法

有返回值:异步方法().Result 无需写await了手动取值
无返回值:异步方法().Wait() File.WriteAllTextAsync(path,”Albert”).Wait();
容易造成死锁,谨慎使用

  1. //手动取值
  2. string s2 = File.ReadAllTextAsync(fileName).Result;
  3. //Wait(),无返回值的情况
  4. File.WriteAllTextAsync(path,"Albert").Wait();

四 异步委托:异步Lambda表达式

在委托中使用,异步Lambda表达式 在obj前面加上async。

  1. //异步Lambda表达式
  2. ThreadPool.QueueUserWorkItem(async obj =>
  3. {
  4. await File.WriteAllTextAsync(fileName, "xxxx");
  5. });

五 Async Await原理揭秘

  1. using System;
  2. using System.Net.Http;
  3. using System.Threading.Tasks;
  4. using System.IO;
  5. static async Task Main(string[] args){
  6. using(HttpClient httpClient = new HttpClient()){
  7. string html = await httpClient.GetStringAsync("http://www.google.com");
  8. Console.WriteLine(html);
  9. }
  10. string txt = "Hello ALbert";
  11. string filename = @"E:/albert.txt";
  12. await File.WriteAllTextAsync(filename,txt);
  13. Console.WriteLine("写成功");
  14. string s = await File.ReadAllTextAsync(filename);
  15. Console.WriteLine("读成功”);
  16. }

语法糖,最终编译成状态机的调用。async方法会被C#编译器编译成一个类,会根据await调用进行切分为多个状态,对async方法的调用会被拆分为对MoveNext的调用。

六 Async Await背后的线程调度

await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码。(未必是同一个线程)Tread.CurrentThread.ManagedThreadID
很多个客户点菜,给你递菜单的服务员等你点好呼叫服务员,可能是之前那个,也可能不是。而不需要像多线程那样每个客人对应一个服务员。线程可以得到充分的利用。到要等待的时候,如果发现已经执行结束了,就没必要切换线程浪费开销了。

异步执行的代码并不自动在新线程中执行,除非手动把代码放到新线程中执行。Task.Run(()=>{});可以切线程。

  1. static async Task<double> ReadRandom()
  2. {
  3. //同步写法:
  4. //Console.WriteLine($"我是主线程:{Thread.CurrentThread.ManagedThreadId}");
  5. //double s = new Random().NextDouble();
  6. //return s;
  7. //使用await Task.Run(()=>{})开启异步,跨线程池
  8. //自动匹配返回值类型
  9. return await Task.Run(() => {
  10. Console.WriteLine($"我是主线程:{Thread.CurrentThread.ManagedThreadId}");
  11. double s = new Random().NextDouble();
  12. return s;
  13. });
  14. }

截屏2021-07-06 上午2.19.12.png

七 无async的异步方法

async把返回值包装成Task 如果return的直接是Task则无需async。
async会生成一个类,然后通过状态机MoveNext()进行状态的切换,可能造成线程切换和资源消耗。如果在有必要情况下,不写async。不做拆完后再装的操作。只是一个普通的方法调用。性能效率更高,不会造成线程资源的浪费。

如果一个异步方法只是对别的异步方法的调用的转发,并没有太多复杂的逻辑(比如等待A的结果,再调用B;把A调用的返回值拿到内部做一些处理再返回),那么就可以去掉async关键字。

  1. static async Task<string> ReadFileAsync(int num)
  2. {
  3. switch (num)
  4. {
  5. //ReadAllTextAsync返回Task<string> await将string值取出来,返回给调用
  6. //函数ReadFileAsync,ReadFileAsync通过async将string包装成Task<string>
  7. case 1:
  8. return await File.ReadAllTextAsync(@"E:\albert.txt");
  9. case 2:
  10. return await File.ReadAllTextAsync(@"E:\2.txt");
  11. default:
  12. throw new ArgumentException("num invalid");
  13. }
  14. }
  15. static Task<string> ReadFileSecondAsync(int num)
  16. {
  17. switch (num)
  18. {
  19. //ReadAllTextAsync返回Task<string> await将string值取出来,返回给调用
  20. //函数ReadFileAsync,ReadFileAsync通过async将string包装成Task<string>
  21. case 1:
  22. return File.ReadAllTextAsync(@"E:\albert.txt");
  23. case 2:
  24. return File.ReadAllTextAsync(@"E:\2.txt");
  25. default:
  26. throw new ArgumentException("num invalid");
  27. }
  28. }

八 不要Thread.Sleep()

利用await Task.Delay(TimeSpan.FromSeconds(2))来替代Thread.Sleep()
Thread.Sleep()会阻塞当前线程
await Task.Delay()方式可以有效减少线程的消费,是创建高伸缩性的服务器程序的关键。
在winform程序中,点击事件可以改写成异步方法,但是void不需要变为Task,因为那是事件(委托)规定好的

Button_Click不是在主线程中,而是异步线程,如果用的是Thread.Sleep()则会阻塞主线程
await Task.Sleep()阻塞的是异步线程。Sleep会阻塞Web服务器线程,线程会卡住。

  1. private async void Button1_Click(object sender,EventArgs e){
  2. using(HttpClient httpClient = new HttpClient()){
  3. string s1 = await httpClient.GetStringAsync("http://www.baidu.com");
  4. this.textbox1.Text = s1.Substring(0,20);
  5. //Thread.Sleep(3)阻塞的是主线程
  6. await Task.Delay(TimeSpan.FromSeconds(3));//阻塞的是MoveNext切状态
  7. string s2 = await httpClient.GetStringAsync("http://www.baidu.com");
  8. this.textbox1.Text = s2.Substring(0,20);
  9. }
  10. }

九 CancellationToken

在ASP.NET开发中,只要有cancellationtoken参数就传入。Action方法都传入CancellationToken token参数,调用异步方法能传token都传这个参数。

  1. namespace _210712_Demo01_Aysnc
  2. {
  3. class Program
  4. {
  5. static async Task Main(string[] args)
  6. {
  7. CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
  8. cancellationTokenSource.CancelAfter(5000);
  9. await DownloadHtml("https://www.baidu.com", 10000, cancellationTokenSource.Token);
  10. }
  11. static async Task DownloadHtml(string url,int downloadCtn,CancellationToken cancellationToken)
  12. {
  13. using(HttpClient httpClient = new HttpClient())
  14. {
  15. for (int i = 0; i < downloadCtn; i++)
  16. {
  17. string html = await httpClient.GetStringAsync(url);
  18. Console.WriteLine($"{DateTime.Now}:{html}");
  19. //第二种方式html内容的方式,可能在5s没到就取消请求了,而不需要一直等到请求后才能处理
  20. var resp = await httpClient.GetAsync(url, cancellationToken);
  21. string htmlTwo = await resp.Content.ReadAsStringAsync();
  22. //此种方案无法实现立即取消
  23. //if (cancellationToken.IsCancellationRequested)
  24. //{
  25. // Console.WriteLine("请求被取消");
  26. // break;
  27. //}
  28. }
  29. }
  30. }
  31. }
  32. }
  33. public async Task<IActionResult> Index(CancellationToken token)
  34. {
  35. await DownloadAsync("https://www.baidu.com", 10000, token);
  36. return View();
  37. }

十 Task重要方法

Task.WhenAny(IEnumerable tasks),任何一个Task完成,Task就完成。
Task.WhenAll(params Task[] tasks),所有Task完成,Task才算完成,用于等待多个任务执行结束,但是不在乎执行顺序。
//开启n个Task,然后将Task放在Task数组中,用WhenAll等待所有数组Task执行完毕

  1. namespace _20170713_Demon01_TaskWhenAll
  2. {
  3. class Program
  4. {
  5. static async Task Main(string[] args)
  6. {
  7. string[] filename = Directory.GetFiles(@"C:\01_AlbertCode\NET\NETCoreWinform");
  8. Task<int>[] countTask = new Task<int>[filename.Length];
  9. //开启n个Task,然后将Task放在Task数组中,用WhenAll等待所有数组Task执行完毕
  10. for (int i = 0; i < filename.Length; i++)
  11. {
  12. string file = filename[i];
  13. Task<int> t = ReadCharCount(file);
  14. countTask[i] = t;
  15. }
  16. int[] counts = await Task.WhenAll(countTask);
  17. Console.WriteLine(counts.Sum());//计算数组的和 Linq
  18. Console.ReadLine();
  19. }
  20. //将文件夹下所有文件的字符个数汇总出来
  21. static async Task<int> ReadCharCount(string filename)
  22. {
  23. string s = await File.ReadAllTextAsync(filename);
  24. return s.Length;
  25. }
  26. }
  27. }

FromResult()创建普通数值的Task对象:异步方法中在没有声明async的情况下使用:(容易造成死锁)

  1. //手动取值
  2. string s2 = File.ReadAllTextAsync(fileName).Result;
  3. //Wait(),无返回值的情况
  4. File.WriteAllTextAsync(path,"Albert").Wait();

十一 零碎知识点

  • 接口/抽象类的方法不能修饰为async
  • 异步与yield yield return不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提升性能。yield return按需供给 yield return 会再调用的时候才会供给反编译拆成三段。在旧版,异步方法无法用yield。从C#8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach()即可。 ```csharp //两者等价,第一种方法需要额外开辟空间,而第二个则按需供给。 static IEnumerable Test(){ List list = new List(); list.Add(“Hello”); list.Add(“Yzk”); list.Add(“Youzack”); }

static IEnumerable Test(){ yield return “Hello”; yield return “Yzk”; yield return “Youzack”; }

static IAsyncEnumerable Test(){ yield return “Hello”; yield return “Yzk”; yield return “Youzack”; }

static async Task Main(string[] args){ await foreach(var s in Test()){ Console.WriteLine(s); } } ```

  • ASP.NET Core和控制台项目中没有SynchronizationContext,因此不用管ConfigureAwait(false)等。不要同步异步混用。在Winform和WPF中依旧存在。

十二 疑惑点

1.异步方法可以重载吗?

2.如何避免不支持异步方法调用方造成的死锁?

3.状态机模式的理解?

4.为什么要把一个async方法拆分为多个状态然后分为多次调用?

5.异步的可以避免线程等待耗时操作但是await还是等待呀?

await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码。(未必是同一个线程)Tread.CurrentThread.ManagedThreadID
很多个客户点菜,给你递菜单的服务员等你点好呼叫服务员,可能是之前那个,也可能不是。而不需要像多线程那样每个客人对应一个服务员。线程可以得到充分的利用。到要等待的时候,如果发现已经执行结束了,就没必要切换线程浪费开销了。