第十六节:时隔两年再谈异步及深度剖析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下面的代码

注:这里新的线程,有一点几率是原线程;状态机本身不会产生新的线程。

      CLR内部有个优化,当要 等待【异步操作】完成的时候,如果发现已经执行结束了,那就没必要切换线程了,剩下的代码在之前的线程上继续执行了。

(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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 

 

posted @ 2020-05-08 08:57  Yaopengfei  阅读(2075)  评论(12编辑  收藏  举报