第十六节:时隔两年再谈异步及深度剖析async和await
Posted yaopengfei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第十六节:时隔两年再谈异步及深度剖析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,这时释放当前线程,异步操作不占用线程。
///PS:那么IO操作是靠什么执行的呢? 是以 Windows 事件为基础的,因此不需要新建一个线程或使用线程池里面的线程来执行具体工作.
/// ①.比如上面 SaveChangesAsync, await后,释放当前线程,写入数据库的操作当然是由数据库来做了; 再比如 await WriteLineAsync,释放当前线程,写入文件是调用系统底层的
/// 的API来进行,至于系统Api怎么调度,我们就无法干预了.
/// ②.我们使用的是系统的原生线程,而系统使用的是cpu线程,效率要高的多,我们能做的是尽量减少原生线程的占用.
///(3).好处:
/// A.提高了线程的利用率(即提高系统的吞吐量,提高了系统处理的并发请求数)----------针对IO-Bound场景。
/// PS:一定要注意,是提高了系统的吞吐量,不能提升性能,也不能提高访问速度。
/// 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插入1000条数据为例,调用SaveChangesAsync模拟IO-Bound场景,经测试tId1和tId2是不同的(也有一定几率是相同的),这就说明了await的时候当前线程已经释放了.
///(直接在主线程中写EF的相关的代码 和 将相关代码封装成一个异步方法IOTestAsync 效果一样)
///(4).异步不回掉的场景:自己封装一个异步方法,然后在接口中调用,注意调用的时候不加await,经测试主线程进入异步方法内,走到第一个await的时候会立即返回外层,继续方法调用下面的代码。
///(如下面:TBDataAsync方法,主线程快速执行完, 异步方法还在那自己继续执行,前提:要求该异步方法中不能有阻塞性的代码!!!!)
/// 如何理解呢?
/// 主线程调用该异步方法,相当于执行一个任务,因为调用的时候没有加await,所以不需要等待,即使异步方法内部会等待,但那已经是另外一个任务了,主线程本身并没有等待这个任务,任务里的await
/// 那是任务自己事.
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
以上是关于第十六节:时隔两年再谈异步及深度剖析async和await的主要内容,如果未能解决你的问题,请参考以下文章