zoukankan      html  css  js  c++  java
  • 第十六节:时隔两年再谈异步及深度剖析async和await(新)

    一. 再谈异步

    1. 什么是异步方法

     使用者发出调用指令后,不需要等待返回值,就可以继续执行后面的代码,异步方法基本上都是通过回调来通知调用者。

     (PS:线程池是一组已经创建好的线程,随用随取,用完了不是销毁线程,然后放到线程池中,供其他人用)

     异步方法可以分为两类:

     (1).CPU-Bound(计算密集型任务):以线程为基础,具体是用线程池里的线程还是新建线程,取决于具体的任务量。

     (2).I/O-Bound(I/O密集型任务):是以windows事件为基础(可能调用系统底层api),不需要新建一个线程或使用线程池里面的线程来执行具体工作,不涉及到使用系统原生线程。

    2. .Net异步编程历程

    (1).EAP

     基于事件的编程模型,会有一个回调方法,EAP 是 Event-based Asynchronous Pattern(基于事件的异步模型)的简写,类似于 Ajax 中的XmlHttpRequest,send之后并不是处理完成了,而是在 onreadystatechange 事件中再通知处理完成。

     优点是简单,缺点是当实现复杂的业务的时候很麻烦,比如下载 A 成功后再下载 b,如果下载 b成功再下载 c,否则就下载 d。

     EAP 的类的特点是:一个异步方法配一个*** Completed 事件。.Net 中基于 EAP 的类比较少。也有更好的替代品,因此了解即可。

    相关代码:

    WebClient wc = new WebClient(); 
    wc.DownloadStringCompleted += Wc_DownloadStringCompleted; 
    wc.DownloadStringAsync(new Uri("http://www.baidu.com")); 
    private void Wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) 
    { 
     MessageBox.Show(e.Result); 
    } 

    (2).APM

      APM(Asynchronous Programming Model)是.Net 旧版本中广泛使用的异步编程模型。使用了 APM 的异步方法会返回一个 IAsyncResult 对象,这个对象有一个重要的属性 AsyncWaitHandle,他是一个用来等待异步任务执行结束的一个同步信号。

      APM 的特点是:方法名字以 BeginXXX 开头,返回类型为 IAsyncResult,调用结束后需要EndXXX。 .Net 中有如下的常用类支持 APM:Stream、SqlCommand、Socket 等。(写博客的时候补充一下代码,如鹏)

    相关代码:

    FileStream fs = File.OpenRead("d:/1.txt"); 
    byte[] buffer = new byte[16]; 
    IAsyncResult aResult = fs.BeginRead(buffer, 0, buffer.Length, null, null); 
    aResult.AsyncWaitHandle.WaitOne();//等待任务执行结束 
    MessageBox.Show(Encoding.UTF8.GetString(buffer)); 
    fs.EndRead(aResult); 
    
    // 如果不加aResult.AsyncWaitHandle.WaitOne() 那么很有可能打印出空白,因为 BeginRead只是“开始读取”。调用完成一般要调用EndXXX 来回收资源。 

    (3).TAP(也有叫TPL的)

      它是基于任务的异步编程模式,一定要注意,任务是一系列工作的抽象,而不是线程的抽象.也就是说当我们调用一个XX类库提供的异步方法的时候,即使返回了Task/Task<T>,我们应该认为它是开始了一个新的任务,而不是开启了一个新的线程(TAP 以 Task 和 Task<T> 为基础。它把具体的任务抽象成了统一的使用方式。这样,不论是计算密集型任务,还是 I/O 密集型任务,我们都可以使用 async 、await 关键字来构建更加简洁易懂的代码)

    相关代码:

     FileStream fs = File.OpenRead("d:/1.txt"); 
     byte[] buffer = new byte[16]; 
     int len = await fs.ReadAsync(buffer, 0, buffer.Length); 
     MessageBox.Show("读取了" + len + "个字节"); 
     MessageBox.Show(Encoding.UTF8.GetString(buffer)); 

    3. 剖析计算密集型任务和 I/O密集型任务

     (1).计算密集型:await一个操作的时候,该操作通过Task.Run的方式启动一个线程来处理相关的工作。当工作量大的时候,我们可以采用Task.Factory.StartNew,可以通过设置TaskCreateOptions.LongRunning选项 可以使新的任务运行于独立的线程上,而非使用线程池里面的线程。

     (2).I/O密集型: await一个操作的时候,虽然也返回一个Task或Task<T>,但这时并不开启线程。

    4.如何区分计算密集型任务还是I/O密集型任务?

     计算密集型任务和I/O密集型任务的异步方法在使用上没有任何差别,但底层实现却大不相同, 判断是计算型还是IO型主要看是占用CPU资源多 还是 占用I/O资源多。

     比如:获取某个网页的内容

    // 这是在 .NET 4.5 及以后推荐的网络请求方式
    HttpClient httpClient = new HttpClient();
    var result = await httpClient.GetStringAsync("https://www.qq.com");
    
    // 而不是以下这种方式(虽然得到的结果相同,但性能却不一样,并且在.NET 4.5及以后都不推荐使用)
    WebClient webClient = new WebClient();
    var resultStr = Task.Run(() => {
        return webClient.DownloadString("https://www.qq.com");
    });

     比如:排序,属于计算密集型任务

    Random random = new Random();
    List<int> data = new List<int>();
    for (int i = 0; i< 50000000; i++) {
        data.Add(random.Next(0, 100000));
    }
    // 这儿会启动一个线程,来执行排序这种计算型任务
    await Task.Run(() => {
        data.Sort();
    });

      所以我们在自己封装的异步方法的时候,一定要注意任务的类型,来决定是否开启线程。

    5. TAP模式编码注意事项

    (先记住套路,后面通过代码写具体应用)

     (1).异步方法返回Task或者Task<T>, 方法内部如果是返回void,则用Task; 如果有返回值,则用Task<T> ,且不要使用out和ref.

     (2).async和await要成对出现,要么都有,要么都没有,await不要加在返回值为void的前面,会编译错误.

     (3).我们应该使用非阻塞代码来写异步任务.

      应该用:await、await Task.WhenAny、 await Task.WhenAll、await Task.Delay.

      不要用:Task.Wait 、Task.Result、Task.WaitAny、Task.WaitAll、Thread.Sleep.

     (4).如果是计算密集型任务,则应该使用 Task.Run 来执行任务;如果是耗时比较长的任务,则应该使用 Task.Factory.StartNew 并指定 TaskCreateOptions.LongRunning选项来执行任务如果是 I/O 密集型任务,不应该使用 Task.Run.

     (5). 如果是 I/O 密集型任务不应该使用 Task.Run!!! 因为 Task.Run 会在一个单独的线程中运行(线程池或者新建一个独立线程),而对于 I/O 任务来说,启用一个线程意义不大,反而会浪费线程资源.

    二. 深剖async和await

    1.说明

     async和await是一种异步编程模型,用于简化代码,达到“同步的方式写异步的代码”,编译器会将async和await修饰的代码编译成状态机,它们本身是不开启线程的

      (async和await一般不要用于winform窗体程序,会出现一些意想不到的错误)

    2.深层理解

    (1).async和await只是一个状态机,执行流程如下: await时释放当前线程(当前线程回到线程池,可供别人调用)→进入状态机等待【异步操作】完成→退出状态机,从线程池中返回一个新的线程执行await下面的代码(这里新的线程,有一点几率是原线程;状态机本身不会产生新的线程)

    (2).异步操作分为两种

     A.CPU-Bound(计算密集型):比如 Task.Run ,这时释放当前线程,异步操作会在一个新的线程中执行。

     B.IO-Bound(IO密集型):比如一些非阻止Api, 像EF的SaveChangesAsync、写文件的WriteLineAsync,这时释放当前线程,异步操作不占用线程。

    那么IO操作是靠什么执行的呢?

     是以 Windows 事件为基础的,因此不需要新建一个线程或使用线程池里面的线程来执行具体工作。

    ①.比如上面 SaveChangesAsync, await后,释放当前线程,写入数据库的操作当然是由数据库来做了; 再比如 await WriteLineAsync,释放当前线程,写入文件是调用系统底层的 的API来进行,至于系统Api怎么调度,我们就无法干预了.

    ②.我们使用的是系统的原生线程,而系统使用的是cpu线程,效率要高的多,我们能做的是尽量减少原生线程的占用.

    (3).好处

     A.提高了线程的利用率(即提高系统的吞吐量,提高了系统处理的并发请求数)----------针对IO-Bound场景。

      一定要注意,是提高了系统的吞吐量,不能提升性能,也不能提高访问速度。

     B.多线程执行任务时,不会卡住当前线程--------------------------------------针对CPU-Bound场景。

    3. IO-Bound异步对服务器的意义

     每个服务器的工作线程数目是有限的,比如该服务器的用于处理项目请求的线程数目是8个,该cpu是单核,那么这8个线程在做时间片切换,也就是我们所谓的并发;假设该服务器收到了9个并发请求, 每个请求都要执行一个耗时的IO操作,下面分两种情况讨论:

     (1).如果IO操作是同步,那么会有8个线程开始并发执行IO操作,第9个请求只能在那等待,必须等着这个8个请求中的某一个执行完才能去执行第9个请求,这个时候我们设想并发进来20个请求甚至 更多,从第9个开始,必须排队等待,随着队列越来越长,服务器开始变慢,当队列数超过IIS配置的数目的时候,会报503错误。

     (2).如果IO操作是异步的,并且配合async和await关键字,同样开始的时候8个线程并发执行IO操作,线程走到await关键字的时候,await会释放当前线程,不再占用线程,等待异步操作完成后,再重新去线程池中分配一个线程;从而await释放的当前线程就可以去处理别的请求,依次类推,线程的利用率变高了,也就是提高了系统处理的并发请求数(也叫系统的吞吐量).

    4.测试

     (1).同步场景:主线程会卡住不释放,tId1和tId2的值一定相同。

     (2).CPU-Bound场景: 利用Task.Run模拟耗时操作,经测试tId1和tId2是不同的(也有一定几率是相同的),这就说明了await的时候当前线程已经释放了.

     (如下的:CalData2方法 和 CalData3Async方法)

     (3).IO-Bound场景: 以EF插入10000条数据为例,调用SaveChangesAsync模拟IO-Bound场景,经测试tId1和tId2是不同的(也有一定几率是相同的),这就说明了await的时候当前线程已经释放了.

     (直接在主线程中写EF的相关的代码 和 将相关代码封装成一个异步方法IOTestAsync 效果一样)

     (4).异步不回掉的场景:自己封装一个异步方法,然后在接口中调用,注意调用的时候不加await,经测试主线程进入异步方法内,走到第一个await的时候会立即返回外层,继续方法调用下面的代码。

     (如下面:TBDataAsync方法,主线程快速执行完, 异步方法还在那自己继续执行,前提:要求该异步方法中不能有阻塞性的代码!!!!)

    这种异步不回调的场景如何理解呢?

     主线程调用该异步方法,相当于执行一个任务,因为调用的时候没有加await,所以不需要等待,即使异步方法内部会等待,但那已经是另外一个任务了,主线程本身并没有等待这个任务,任务里的await那是任务自己事.

    相关代码:

            /// <summary>
            /// 剖析async和await  
            /// </summary>
            /// <returns></returns>
            public async Task<IActionResult> Index()
            {
                var tId1 = Thread.CurrentThread.ManagedThreadId;
    
                #region 01-同步场景        
                //{
                //    CalData1();
                //}
                #endregion
    
                #region 02-CPU-Bound场景
                //{            
                //    await Task.Run(() =>
                //    {
                //        //模拟耗时操作 
                //        Thread.Sleep(3000);
                //    });
    
                //    //等价于上面的代码
                //    //await CalData2Async();
                //}
                #endregion
    
                #region 03-CPU-Bound场景(自己封装异步方法)
                //{
                //    int result = await CalData3Async();
                //} 
                #endregion
    
                #region 04-IO-Bound场景1
                //{
                //    AsyncDBContext context = new AsyncDBContext();
                //    for (int i = 0; i < 10000; i++)
                //    {
                //        UserInfor uInfor = new UserInfor()
                //        {
                //            id = Guid.NewGuid().ToString("N"),
                //            userName = "ypf",
                //            addTime = DateTime.Now
                //        };
                //        await context.AddAsync(uInfor);
                //        //context.Add(uInfor);
                //    }
                //    await context.SaveChangesAsync();
                //}
                #endregion
    
                #region 05-IO-Bound场景2(封装成异步方法)
                //{
                //    int result = await IOTestAsync();
                //}
                #endregion
    
                #region 06-异步不等待的场景
                //{
                //    TBDataAsync();
                //}
    
                #endregion
    
                var tId2 = Thread.CurrentThread.ManagedThreadId;
                ViewBag.tId1 = tId1;
                ViewBag.tId2 = tId2;
                return View();
            }
    
    
            /// <summary>
            /// 同步场景-模拟耗时运算
            /// </summary>
            /// <returns></returns>
            public void CalData1()
            {
                //模拟耗时操作
                Thread.Sleep(4000);
            }
    
    
            /// <summary>
            /// 模拟耗时运算-一异步方法
            /// (本身不会被编译成状态机)
            /// </summary>
            /// <returns></returns>
            public Task CalData2Async()
            {
                var task = Task.Run(() =>
                {
                    Thread.Sleep(3000);
                });
                return task;
            }
    
            /// <summary>
            /// 模拟耗时操作,封装成异步方法
            /// (本身会被编译成状态机)
            /// </summary>
            /// <returns></returns>
            public async Task<int> CalData3Async()
            {
                var result = await Task.Run(() =>
                  {
                      //耗时操作
                      Thread.Sleep(3000);
                      return 100;
                  });
                return result;
            }
    
    
            /// <summary>
            /// IO-Bound场景(封装成异步方法)
            /// </summary>
            /// <returns></returns>
            public async Task<int> IOTestAsync()
            {
                AsyncDBContext context = new AsyncDBContext();
                for (int i = 0; i < 10000; i++)
                {
                    UserInfor uInfor = new UserInfor()
                    {
                        id = Guid.NewGuid().ToString("N"),
                        userName = "ypf",
                        addTime = DateTime.Now
                    };
                    await context.AddAsync(uInfor);
                    //context.Add(uInfor);
                }
                return await context.SaveChangesAsync();
            }
    
    
            /// <summary>
            /// 模拟耗时IO操作-用于测试异步不等待场景
            /// </summary>
            /// <returns></returns>
            public async Task<int> TBDataAsync()
            {
                var tId2 = Thread.CurrentThread.ManagedThreadId;
                AsyncDBContext context = new AsyncDBContext();
    
                await Task.Delay(7000);  //模拟耗时操作,同步调用的时候,主线程走到这立即返回,接着走主线程的任务 
    
                //一个新的线程执行后面的代码
                Console.WriteLine($"线程id为:{Thread.CurrentThread.ManagedThreadId}");
    
                var list = await context.Set<UserInfor>().ToListAsync();
                foreach (var item in list)
                {
                    item.userName = "001";
                }
                return await context.SaveChangesAsync();
            }

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    oracle之check约束小结
    非归档模式下使用Rman进行备份和恢复
    R中,定义一个长度为0的向量
    R中,去掉dataframe中的NA行
    Oracle数据库的后备和恢复————关于检查点的一些知识
    关于oracle修复控制文件与数据文件不一致的问题----
    《SLAM机器人基础教程》第三章 单片机与STM32:GPIO实验及Keil软件使用WatchWindows进行Debug调试
    《SLAM导航机器人基础》第三章:单片机与STM32:单片机概述和Keil开发环境配置
    《SLAM导航机器人基础》第二章:C/C++编程(后)
    《SLAM导航机器人基础》第二章:C/C++编程(中)
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/12848392.html
Copyright © 2011-2022 走看看