码迷,mamicode.com
首页 > 其他好文 > 详细

第十六节:时隔两年再谈异步及深度剖析async和await

时间:2020-05-08 09:48:16      阅读:67      评论:0      收藏:0      [点我收藏+]

标签:分配   因此   one   简单   返回值   责任   简写   场景   属性   

一. 再谈异步

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

标签:分配   因此   one   简单   返回值   责任   简写   场景   属性   

原文地址:https://www.cnblogs.com/yaopengfei/p/12848392.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!