C# .Net Core (5.0)异步---笔记

异步

笔记来源于B站杨中科老师的.NET 5.0教学

引言

在现实生活中,我们迫切的需要做一件事,尤其是需要等待的时候,"异步"的例子在这里体现的尤为强烈.

当我们口渴时,需要等待一杯温开水来解我们于口渴之中.这时,等待这个关键词就体现出了我们要同步等待水开,并且,其他事请我们都做不了,刷个短视频,打两把游戏.都不行.

但我们在等待的同时,也继续着手中的游戏,短视频,这个就成为了异步.

同步->异步,这个过程真的使我们所消耗的时间减少了?

并没有,因为事情还是那个事情,没做完,我们也不能上赶子的去要结果.它只是提高了我们在同一时刻可以做多个事情.

正文

在C#中,通过2个关键字,一个特殊后缀特殊返回值来加以修饰异步的特性.规则如下:

  1. await
  2. async
  3. 对应的异步方法,后缀名+Async
  4. 返回值类型Task类型.

终将还是要回到代码上的,在代码中,异步的场景主要体现在,做一个相当大的耗时操作时,由于CPU执行指令是顺序执行,会导致出些一些异常.

在进行一些大文件的读写操作时,会因为没有使用异步的情况下,导致程序以报异常的形式出场.

使用异步方法

static async Task Main(string[] args)
{
 	string filename = @".\1.txt";


    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < 10000000; i++)
        sb.AppendLine("asfasfddsafsafjsaljfsajfsalfj");
    await File.WriteAllTextAsync(filename, sb.ToString());
    //若在对应的异步方法添加await会在此有等待.
    Console.WriteLine("Write after...");

    string content = await File.ReadAllTextAsync(filename);
    Console.WriteLine("read after...");   
}

这里是进行了一个较大文件的读写操作,如果不适用异步的话,那么在读取文件时就会出问题.

首先,File.WriteAllTextAsync();时一个异步方法,我们可以看到,前面构造了长度为n的字符串进行了n*10000000次的构造(数据量相当惊人).其后,我们开始对此进行了写入文件的操作.

上述,File.WriteAllTextAsync();方法是一个异步方法,所以需要用await加以修饰.期间,会一直等待此异步方法执行完毕.

其后,随着一波控制输出,就开始了读文件,与上述相似,需要加上await来取出执行后的结果.

小问题

尚且认为第一个异步方法可以不加await,如下:

static async Task Main(string[] args)
{
 	string filename = @".\1.txt";


    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < 10000000; i++)
        sb.AppendLine("asfasfddsafsafjsaljfsajfsalfj");
    File.WriteAllTextAsync(filename, sb.ToString());
    //若在对应的异步方法添加await会在此有等待.
    Console.WriteLine("Write after...");

    string content = await File.ReadAllTextAsync(filename);
    Console.WriteLine("read after...");   
}

那么,write这里就会出现同步情况,其WriteAllTextAsync()内部是一个子线程的阻塞,所以,这里就会执行到```ReadAllTextAsync()` ``的位置,这时就会报出一个文件还在写就开始读的错误.

await就会加以等待该方法执行完毕.

记忆方法

await可以等待返回值Task的异步方法,加完之后,对应的方法需要async,并且修改返回值为Task

异步方法的委托使用

线程池对象:ThreadPool

ThreadPool.QueueUserWorkItem(async(obj) =>
            {
                StringBuilder sb = new StringBuilder();
                string fileName = @"./2.txt";
                for (int i = 0; i < 10000000; i++)
                {
                    sb.AppendLine("djsalflsajflsafsaf");
                }

                while (true)
                {
                    await File.WriteAllTextAsync(fileName, sb.ToString());
                }
            });

首先如果拉姆达函数体内有await关键字,那么在一个拉姆达函数形参的位置处加上async来进行修饰.

异步方法的编写

不知道写啥,上来就是如下:

返回值:Task

方法名:Async结尾

如果异步方法体内有await关键词出现,在该异步方法前用async关键词修饰.

特殊情况下的异步使用

在某些比较老的框架,如(.NET framework),可能要求Main方法返回值必须为void.winForm的事件函数中,也要求返回值为void

这个时候,有Wait() -> await

Result -> 取值

缺点:由于方法年代较早,使用过程中可能会出现死锁的 情况,需慎重使用

Wait()可以替代异步等待的作用,Task.Result就相当于取值的作用.

async Main的神秘面纱

static async Task Main(string[] args){
 ...;   
}

如上的异步Main,其实已经不是底层在调用的那个Main了,通过IL反编译出可执行程序的dll文件就会发现,这个Main被以异步的方法进行调用了,并且内部所执行的所有await也都被反编译成了类.

系统会以普通的Main来调用执行异步Main.并进行GetWait(),GetResult()

异步Main将会成为一个类,然后利用状态机模型(不同的返回值,不同的状态)来进行对应的执行.这是await背后的事情.

所以await,async就是所谓的C#语法糖.

async背后的线程切换

await所修饰的函数为界,前面的与后面的代码段所执行的线程可能是不一样的.

原理:await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码.

处理的数据量较小时,前后线程号是一样的.

关于await的一些误区

误区一:await所执行的代码,或者是我们自己编写的代码,并不是单独放在一个线程中进行执行的.

代码如下:

static async Task Main(string[] args)
{


    Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");

    await HandlerAsync();

    Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
}

static async Task HandlerAsync()
{
    Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");

    while (true);
}

通过执行后可以发现,异步方法中的线程号一直都是之前所使用的线程号.

真正的切换线程号是在进行Task使用时更换线程的,代码如下:

static async Task Main(string[] args)
{
    Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
    await Task.Run(() =>
                   {
                       Console.WriteLine($"Run()中的线程号:{Thread.CurrentThread.ManagedThreadId}");
                       while (true);
                   });

    Console.WriteLine($"当前线程号:{Thread.CurrentThread.ManagedThreadId}");
}

异步方法缺点

  1. 异步方法会被CLR生成一个类,运行效率没有普通方法高.
  2. 可能会占用非常多的线程.

小问题

为何有的方法本身就是异步的,但在声明该方法时,却没有在函数头标注async呢?

简单理解如下:在执行异步方法后会返回一个Task的值,如果不着急将其result返回的话(即没有在调用该异步方法前面标注await时),就可以不在前面标注为async.

换言之,async具有将一个类型T给包装为Task的泛型类型,具有一定的承上启下的作用.

如C#中,File.ReadAllTextAsync()方法.

public static Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default);

实验

我们知道ReadAllTextAsync的返回值为Task,通过我们初次对它的理解,可以得知,该返回值可以通过await进行调用.

static async Task Main(string[] args)
{
    string c= await MyReadAsync(1);
    Console.WriteLine(c);
}

static Task<string> MyReadAsync(int n)
{
    switch (n)
    {
        case 1:
            return File.ReadAllTextAsync(@"./1.txt");//直接返回执行结果Task<string>
        case 2:
            return File.ReadAllTextAsync(@"./2.txt");
        default:
            throw new ArgumentException();
    }
}

若我们在返回结果上,加上await取出result时,就必须需要在函数前标注async,如下:

static async Task<string> MyReadAsync(int n)
{
    switch (n)
    {
        case 1:
            return await File.ReadAllTextAsync(@"./1.txt");
        case 2:
            return await File.ReadAllTextAsync(@"./2.txt");
        default:
            throw new ArgumentException();
    }
}

但是,外部依然可以调用,这就可以发现,C#的语法糖赋予async的一种行为就是,在把结果返回后,async具有将其具体类型值包装为Task的形式.

若是需要将一个比较耗时的任务放到线程中进行执行的话,并得到结果的话,那么它的代码应该是这样的.

static async Task<int> Main(string[] args)
{

    return await Task.Run(() =>
                          {
                              int result = 1;
                              for (int i = 1; i < 5; i++)
                              {
                                  result *= i;
                              }
                              return result;
                          });
}

可以看到,Task.Run()方法,前面已经加以await的修饰了,这就意味着该方法也是返回了Task的这种类型.那么就会有async也将其result包装的作用.

若不着急使用结果,我们就可以不进行await,从而省去了async的简写.

static  Task<int> Main(string[] args)
        {

            return Task.Run(() =>
            {
                int result = 1;
                for (int i = 1; i < 5; i++)
                {
                    result *= i;
                }
                return Task.FromResult(result);
            });
        }

通过这里可以发现,async具有转发异步执行的作用,await具有取出result并作进一步的数据加工的含义.这样才能够使async更进一步的连接好下一操作流程的异步处理.

异步编程是否能用Thread.Sleep()?

若在异步方法中想睡眠一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await Task.Delay();

作业:
封装一个异步方法,下载给
定的网址,如果下载失败,
则稍等500ms再重试,如果
重试三次仍然失败,则抛异
常“下载失败”

封装为异步方法:

private async Task DownloadWebPage(string uri)
        {
            int cnt = 0;
            using (HttpClient httpClient = new HttpClient())
            {
                again:
                string content = default(string);
                try
                {
                    content = await httpClient.GetStringAsync(uri);
                    //Thread.Sleep(3000);   //有明显的阻塞
                    await Task.Delay(500);//很流畅
                    textBox1.Text = content;
                }
                catch (Exception ex)
                {
                    if(ex.GetType() is HttpRequestException)
                    {
                        //重连次数.
                        if(cnt<3)
                        {
                            MessageBox.Show(ex.Message);
                            cnt++;
                            //实验Thread.Sleep()和Task.Delay()
                            //Thread.Sleep(3000);
                            await Task.Delay(500);
                            goto again;
                        }
                        else
                        {
                            throw new Exception("下载失败!");

                        }
                    }
                }

            }
        }

使用Task.Run()方法,即开启子线程的方式

string c = await Task.Run(async () =>
              {
                  int cnt = 0;
                  using (HttpClient httpClient = new HttpClient())
                  {
                  again:
                      string content = default(string);
                      try
                      {

                          content = await httpClient.GetStringAsync("https://www.bilibili.com/");
                          //两种睡眠方法在使用时均无很大的差别,但性能上Task.Delay();方法的使用会比Sleep好的多.
                        //Thread.Sleep(3000);
                        await Task.Delay(500);
                      }
                      catch (Exception ex)
                      {

                          if (ex.GetType() is HttpRequestException)
                          {
                            //重连次数.
                            if (cnt < 3)
                              {
                                  MessageBox.Show(ex.Message);
                                  cnt++;
                                //实验Thread.Sleep()和Task.Delay()
                                //Thread.Sleep(3000);
                                //await Task.Delay(500);
                                goto again;
                              }
                          }


                      }
                      return content;

                  }
              });

异步编程 CancellationToken

作用:可以提高程序的工作效率

应用场景:当用户访问此网页时,由于特殊情况,取消了访问.而在之前向服务器发起的请求,在服务器中一直在计算,无疑浪费了服务器的计算资源.

所以,有很多的异步方法都提供了CancellationToken参数,用于获得提前终止执行的信号.

CancellationToken结构体

struct CancellationToken{
    None;//空
    bool isCancellationRequested;//是否取消.
    Register(Action callback);//注册取消监听.
    //若任务被取消,执行到这句话抛异常
    void ThrowCancellationRequest(){
        if(isCancellationRequested){
            throw new Exception();
        }
    }
}

通过CancellationTokenSource来为CancellationToken创建对象.

CancelAfter();超时后发出取消信号

Cancel();发出取消信号

CancellationToken Token;

举个例子,在向某网站发起请求时,超时的时候将终止处理.但终止处理的种类有:

  1. 在达到该时间点时,完成此请求后,立即终止称延时终止;
  2. 立即终止;

就网络编程HttpClient而言,有两种方法可以这样做.

  1. 适用于立即终止的方法:GetAsync(CancellationToken token);
  2. 适用于延时终止的方法:if(CancellationToken .IsCancelRequested)的为真判断.也或者使用抛出异常的形式来结束处理:CancellationToken.ThrowIfCancelRequested()

延时终止处理的两种方法

static async Task Main(string[] args)
        {
            //创建CancellationToken
            //方法1:
            CancellationTokenSource source = new CancellationTokenSource();
            source.CancelAfter(5000);//5s后,终止处理.
            CancellationToken token = source.Token;
            await DownloadAsync("https://www.youzack.com", 100,token);

            Console.ReadLine();
        }

首先使用了CancellationTokenSource进行创建,调用CancelAfter()方法设置5s后的终止时间.并获得其token,然后进行访问.

方法一:使用cancellationToken.IsCancellationRequested

static async Task DownloadAsync(string uri,int n,CancellationToken  cancellationToken)
        {
            using HttpClient httpClient = new HttpClient();
            for (int i = 0; i < n; i++)
            {
                await httpClient.GetStringAsync(uri);
                Console.WriteLine($"第{i}次请求!");

                //方法1:使用cancellationToken.IsCancellationRequested判断是否已经取消,取消就终止处理,退出
                if (cancellationToken.IsCancellationRequested)
                {
                    Console.WriteLine("终止处理!");
                    break;
                }
            }
            
        }

方法二:抛异常的形式,进行处理.cancellationToken.ThrowIfCancellationRequested()

我们可以根据实际情况,来确定是否要对异常信息进行处理来决定是否捕获.

static async Task DownloadAsync(string uri,int n,CancellationToken  cancellationToken)
        {
            using HttpClient httpClient = new HttpClient();
            for (int i = 0; i < n; i++)
            {
                await httpClient.GetStringAsync(uri);
                Console.WriteLine($"第{i}次请求!");
                //方法2:使用ThrowIfCancellationRequested来抛出异常进行终止处理.
                try
                {
                    cancellationToken.ThrowIfCancellationRequested();

                }
                catch (Exception)
                {
                    Console.WriteLine("终止处理!");
                    break;
                }
            }
            
        }

立即终止处理

static async Task DownloadAsync2(string uri,int n,CancellationToken token)
        {
            using HttpClient httpClient = new HttpClient();
            for (int i = 0; i < n; i++)
            {
                try
                {
                    await httpClient.GetAsync(uri, token);//若超时,也将被终止请求.
                    Console.WriteLine($"第{i}次请求!");
                }
                catch (Exception ex)
                {
                    if (ex is TaskCanceledException)
                    {
                        Console.WriteLine("请求终止!");
                        break;
                    }
                }

            }
        }

以上是在进行处理过程中如果也超时了,则立即终止.

这种方式在web程序中,常见于那种即时取消请求的方式,如,按q键取消请求

static async Task Main(string[] args)
        {
            
            CancellationTokenSource source = new CancellationTokenSource();
           
            CancellationToken token = source.Token;
            DownloadAsync2("https://www.youzack.com", 100,token);
            while (Console.ReadKey().Key !=ConsoleKey.Q) ;

            source.Cancel();//取消

            Console.ReadLine();
        }

static async Task DownloadAsync2(string uri,int n,CancellationToken token)
        {
            using HttpClient httpClient = new HttpClient();
            for (int i = 0; i < n; i++)
            {
                try
                {
                    await httpClient.GetAsync(uri, token);
                    Console.WriteLine($"第{i}次请求!");
                }
                catch (Exception ex)
                {
                    Console.WriteLine("请求终止!");
                    break;
                }

            }
        }

通过控制台输入,然后对CancellationTokenSource对象调用Cancel()方法即可.在DownloadAsync2方法中,通过捕获异常的方式来结束.

Core中的真实案例

以MVC为例,当要对CancellationToken进行处理时,通常是这样做的作为控制器行为函数的参数,由Core mvc框架为其传参,根据实际情况,在处理的函数中做操作.

 public async Task<IActionResult> Index(CancellationToken token)
        {
            string uri = "https://www.youzack.com";
            await DownloadAsync2(uri, 10000, token);
            return View();
        }

static async Task DownloadAsync2(string uri, int n, CancellationToken token)
        {
            using HttpClient httpClient = new HttpClient();
            for (int i = 0; i < n; i++)
            {
                try
                {
                    await httpClient.GetAsync(uri,token);
                    Debug.WriteLine($"第{i}次请求!");
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("请求终止!");
                    break;
                }

            }
        }

可以发现我们无法对token进行修改,它是由core mvc框架管辖的,当请求发生状况时,会在GetAsync()方法中立即见效.

若取消token参数在GetAsync()方法中的传递,在取消对当前网页进行访问后,这些操作将一直在执行.非常浪费服务器的资源.

Task的WhenAll()和WhenAny()方法

Task类的重要方法:

Task<Task> WhenAny(lEnumerable<Task> tasks);//任何一个Task完成,Task就完成
Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks);//所有Task完成,Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
FromResult() 创建普通数值的Task对象。

统计一个文件中的所有文件的字符数量总和

static async Task Main(string[] args)
        {
            string[] files=Directory.GetFiles(@"C:\Users\14095\Desktop\大三考试");

            Task<int>[] tasks= new Task<int>[files.Length];//创建length个任务.
            int cnt = 0;
            foreach (var item in files)
            {
                Task<int> t=ReadCharactorCount(item);//逐个启动任务.
                tasks[cnt++] = t;//赋值给任务数组.
            }

            int[] arr=await Task.WhenAll(tasks);//等待完成.

            int sum = arr.Sum();

        }

        static async Task<int> ReadCharactorCount(string file)
        {
            string c=await File.ReadAllTextAsync(file);
            return c.Length;
        }

接口中的异步方法

async用于提示编译器为异步方法中的await代码进行分段处理,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async.

关于yield它是一个流水线式的返回,其内部也是通过状态机实现的,与async类似.

在C#8.0之前的版本中,yield与async无法在一起共同使用.即yield也是语法糖.

8.0之后(包括8.0),提供了一个叫做IAsyncEnumberable的返回使用,注意不要带Task.

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

        static async IAsyncEnumerable<string> Fun1()
        {
            yield return "a";
            yield return "f";
            yield return "d";
        }
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值