本文将解读大家在async和await提高网站处理能力方面还有一些疑问,同时们会做一个async和await在WinForm中的尝试,并且对比在4.5之前的异步编程模式APM/EAP和async/await的区别,最后们还会探讨在不同线程之间交互的问题。
概述
在之前写的一篇关于async和await的前世今生的文章之后,大家似乎在async和await提高网站处理能力方面还有一些疑问,本身也做了不少的尝试。今天们再来回答一下这个问题,同时们会做一个async和await在WinForm中的尝试,并且对比在4.5之前的异步编程模式APM/EAP和async/await的区别,最后们还会探讨在不同线程之间交互的问题。
IIS存在着处理能力的问题,但是WinForm却是UI响应的问题,并且WinForm的UI线程至始至终都是同一个,所以两者之间有一定的区别。有人会问,现在还有人写WinForm吗?好吧,它确是一个比较老的东西呢,不如WPF炫,技术也不如WPF先进,但是从架构层面来讲,不管是Web,还是WinForm,又或是WPF,Mobile,这些都只是表现层,不是么?现在的大型系统一般桌面客户端,Web端,手机,平板端都会涉及,这也是为什么会有应用层,服务层的存在。们在这谈论的ASP.NET MVC,WinForm,WFP,Android/IOS/WP 都是表现层,在表现层们应该只处理与 表现 相关的逻辑,任何与业务相关的逻辑应该都是放在下层处理的。关于架构的问题,们后面再慢慢深入,另外别说没有提示您,们今天还会看到.NET中另一个已经老去的技术Web Service。
还得提示您,文章内容有点长,涉及的知识点比较多,所以,推荐: 先顶后看 ,先顶后看是21世纪看长篇的首选之道,是良好沟通的开端,想知道是什么会让你与众不同吗?想知道为什么上海今天会下这么大的雨么?请记住先顶后看,你顶的不是文章,而是们冒着大雨还要去上班的可贵精神!先顶后看,你值得拥有!
async/await如何提升IIS处理能力
首先响应能力并不完全是说们程序性能的问题,有时候可能程序没有任何问题,而且精心经过优化,可是响应能力还是没有上去,网站性能分析是一个复杂的活,有时候只能靠经验和不断的尝试才能达到比较好的效果。当然们今天讨论的主要是IIS的处理能力,或者也可能说是IIS的性能,但绝非代码本身的性能。即使async/await能够提高IIS的处理能力,但是对于用户来说整个页面从发起请求到页面渲染完成的这些时间,是不会因为们加了async/await之后产生多大变化的。
另外异步的ASP.NET并非只有async/await才可以做的,ASP.NET在Web Form时代就已经有异步Page了,包括ASP.NET MVC不是也有异步的Controller么?async/await 很新,很酷,但是它也只是在原有一技术基础上做了一些改进,让程序员们写起异步代码来更容易了。大家常说微软喜欢新瓶装旧酒,至少们要看到这个新瓶给们带来了什么,不管是任何产品,都不可能一开始就很完美,所以不断的迭代更新,也可以说是一种正确做事的方式。
ASP.NET并行处理的步骤
ASP.NET是如何在IIS中工作的一文已经很介绍了一个请求是如何从客户端到服务器的HTTP.SYS最后进入CLR进行处理的(强烈建议不了解这一块的同学先看这篇文章,有助于你理解本小节),但是所有的步骤都是基于一个线程的假设下进行的。IIS本身就是一个多线程的工作环境,如果们从多线程的视角来看会发生什么变化呢?们首先来看一下下面这张图。注意:们下面的步骤是建立在IIS7.0以后的集成模式基础之上的。
们再来梳理一下上面的步骤:
所有的请求最开始是由HTTP.SYS接收的,HTTP.SYS内部有一个队列维护着这些请求,这个队列的request的数量大于一定数量(默认是1000)的时候,HTTP.SYS就会直接返回503状态(服务器忙),这是们的第一个阀门。
HTTP.SYS把请求交给CLR 线程池中的IO线程
CLR 线程池中的 Worker线程从IO线程中接过请求来处理,IO线程不必等待该请求的处理结果,而是可以返回继续去处理HTTP.SYS队列中的请求。IO线程和Worker线程的数量上限是第二个阀门。
当CLR中正在被处理的请求数据大于一定值(最大并行处理请求数量)的时候,从IO线程过来的请求就不会直接交给Worker线程,而是放到一个进程池级别的一个队列了,等到这个数量小于临界值的时候,才会把它再次交给Worker线程去处理。这是们的第三个阀门。
哪些因素会控制们的响应能力
从上面们提到了几大阀门中,们可以得出下面的几个数字控制或者说影响着们的响应能力。
HTTP.SYS队列的长度
最大IO线程数量和最大Worker线程数量
最大并行处理请求数量
HTTP.SYS队列的长度
这个觉得不需要额外解释,默认值是1000。这个值取决于们们后面CLR IO线程和Worker线程的处理速度,如果它们两个都处理不了,这个数字再大也没有用。因为最后他们会被存储到进程池级别的队列中,所以只会造成内存的浪费。
最大IO线程数量和最大Worker线程数量
这两个值是可以在web.config中进行配置的。
maxIoThreads: 从HTTP.SYS队列中拿请求的最大IO线程数量
maxWorkerThreads: CLR中真实处理请求的最大Worker线程数量
minIoThreads: 从HTTP.SYS队列中拿请求的最小IO线程数量
minWorkerThreads:CLR中真实处理请求的最小Worker线程数量
minIoThreads和minWorkerThreads的默认值是1,合理的加大他们可以避免不必要的线程创建和销毁工作。maxIoThreads如果设置太大的话,或者说不合理的话就会导致多数的request被放到进程池级别的队列中。所以 maxIoThreads和maxWorkerThreads是有一定关系的,假设1个worker线程1s可以处理10个请求,如果们的机器配置只允许们同时处理100个请求,那们合理的maxThreads就是10。但是IO线程并不需要10s去处理一个请求,它比woker线程快,因为它只要从HTTP.SYS的队列那里拿过来就可以了,们假设一个IO线程1s可以处理20个请求,对应100个请求的上限,那们maxIoThreads的合理值应该是5。
最大并行处理请求数量
进程池级别的队列给们的CLR一定的缓冲,这里面要注意的是,这个队列还没有进入到CLR,所以它不会占用们托管环境的任何资源,也就是把请求卡在了CLR的外面。们需要在aspnet.config级别进行配置,们可以在.net fraemwork的安装目录下找到它。一般是 C:\Windows\Microsoft.NET\Framework\v4.0.30319 如果你安装的是4.0的话。
maxConcurrentRequestPerCPU: 每个CPU所允许的最大并行处理请求数量,当CLR中worker线程正在处理的请求之和大于这个数时,从IO线程过来的请求就会被放到们进程池级别的队列中。
maxConcurrentThreadsPerCPU: 设置为0即禁用。
requestQueueLimit:这个就是们HTTP.SYS中队列的那个长度,们可以在web.config/system.web/processModel结点下配置。
async和await 做了什么?
们终于要切入正题了,拿ASP.NET MVC举例,如果不采用async的Action,那么毫无疑问,它是在一个Woker线程中执行的。当们访问一些web service,或者读文件的时候,这个Worker线程就会被阻塞。假设们这个Action执行时间一共是100ms,其它访问web service花了80ms,理想情况下一个Worker线程一秒可以响应10个请求,假设们的maxWorkerThreads是10,那们一秒内总是可响应请求就是100。如果说们想把这个可响应请求数升到200怎么做呢?
有人会说,这还不简单,把maxWorkerThreads调20不就行了么? 其实们做也没有什么 问题,确实是可以的,而且也确实能起到作用。那们为什么还要大费周章的搞什么 async/await呢?搞得脑子都晕了?async/await给们解决了什么问题?它可以在们访问web service的时候把当前的worker线程放走,将它放回线程池,这样它就可以去处理其它的请求了。和IO线程一样,IO线程只负责把请求交给Worker线程或者放入进程池级别的队列,然后又去HTTP.SYS的队列中处理其它的请求。等到web service给们返回结果了,会再到线程池中随机拿一个新的woker线程继续往下执行。也就是说们减少了那一部分等待的时间,充份利用了线程。
们来对比一下使用async/awit和不使用的情况,
不使用async/await: 20个woker线程1s可以处理200个请求。
那转换成总的时间的就是 20 * 1000ms = 20000ms,
其中等待的时间为 200 * 80ms = 16000ms。
也就是说使用async/await们至少节约了16000ms的时间,这20个worker线程又会再去处理请求,即使按照每个请求100ms的处理时间们还可以再增加160个请求。而且别忘了100ms是基于同步情况下,包括等待时间在内的基础上得到的,所以实际情况可能还要多,当然们这里没有算上线程切换的时间,所以实际情况中是有一点差异的,但是应该不会很大,因为们的线程都是基于线程池的操作。
所有结果是20个Worker线程不使用异步的情况下,1s能自理200个请求,而使用异步的情况下可以处理360个请求,立马提升80%呀!采用异步之后,对于同样的请求数量,需要的Worker线程数据会大大减少50%左右,一个线程至少会在堆上分配1M的内存,如果是1000个线程那就是1G的容量,虽然内存现在便宜,但是省着总结是好的嘛,而且更少的线程是可以减少线程池在维护线程时产生的CPU消耗的。
注意:以上数据并非真实测试数据,真实情况一个request的时间也并非100ms,花费在web service上的时间也并非80ms,仅仅是给大家一个思路:),所以这里面用了async和await之后对响应能力有多大的提升和们原来堵塞在这些IO和网络上的时间是有很大的关系的。
看到这里,不知道大家有没有得到点什么。首先第一点们要知道的是async/await不是万能药,不们不能指望光写两个光键字就希望性能的提升。要记住,一个CPU在同一时间段内是只能执行一个线程的。所以这也是为什么async和await建议在IO或者是网络操作的时候使用。们的MVC站点访问WCF或者Web Service这种场景就非常的适合使用异步来操作。在上面的例子中80ms读取web service的时间,大部份时间都是不需要cpu操作的,这样cpu才可以被其它的线程利用,如果不是一个读取web service的操作,而是一个复杂计算的操作,那你就等着cpu爆表吧。
第二点是,除了程序中利用异步,们上面讲到的关于IIS的配置是很重要的,如果使用了异步,请记得把maxWorkerThreads和maxConcurrentRequestPerCPU的值调高试试。