异步编程: Asp.net中使用Async/Await

2017-01-05 11:03:25来源:oschina作者:蓝宇人点击

https://msdn.microsoft.com/en-us/magazine/dn802603.aspx


大多数的讲解Async/Await的在线资源,都是关于客户端编程。难道Async/Await在服务端就没有用武之地么?很多人的回答应该都是“是”。这篇文章介绍了在ASP.NET使用异步请求, 也是为了打造一个最好的在线资源。在这篇文章中我不会再介绍异步编程的Async/Await的语法。我已经在之前的blog做过相关的介绍。这篇文章主要针对异步在ASP.NET的工作方式。


对于像windows Store,Windows desktop,windows phone apps,UWP这样的应用程序,异步主要的优点在于响应能力。这些类型的应用程序使用异步的主要原因是因为保持UI的相应。对于服务器应用程序来说,异步最大的优点是可伸缩性。Node.js可伸缩性关键就在于它天生的异步的特性。.net 开放web接口(OWIN)是基于异步设计的。ASP.NET也是可以异步的。不仅仅UI应用程序使用异步。


同步vs异步请求处理


在跳入异步请求处理这个坑之前,我们先回顾一下ASP.NET处理同步请求的过程。举个例子,假设系统的的请求依赖一些外部的资源,如数据库或者Web AP。当收到一个请求时,ASP.NET从线程池中拿出一个线程去处理这个请求。因为它是同步写,应用程序将会同步调用外部资源。这阻塞了当前线程直到外部资源的调用返回。如下图



图1 同步调用等待外部资源


最终,外部资源调用返回结果,并要求线程继续处理这个请求。当请求处理完成时,发送请求的结果,并且将请求的线程还给线程池。


这种情况还好,但是当你的ASP.NET服务程序接受到请求超过线程池线程数量时,这时候,额外的请求必须等可用线程才可以被处理。



图2 两个线程的服务器处理三个请求


在这种情况下,两个请求分配线程池的线程,每个请求都会调用外部资源,阻塞他们的线程。第三个请求只能等待有可用线程,但是请求已经到达系统。他们处于HTTP 503错误的危险中。


但是,请想想这两点:


第三个请求需要等待线程池返回线程,有两个线程并没有做什么,只是等待外部资源返回,并没有体会共任何的CPU时间。那些线程就这样白白地被浪费了。这是通过异步处理的情况。


而异步请求的处理方式有所不同,当收到一个请求时,ASP.NET从线程池取出线程处理这个请求,当要调用外部资源时,将采取异步调用的方式,它将这个线程返回给线程池,直到对外部资源的调用返回。 如图3 有两个线程的线程池,当异步请求外部资源时。



图3 异步调用外部资源


最重要的不同点是当进行异步调用时刚开始处理请求的线程已经返回线程池不在于这个请求关联。当外部资源调用返回时,ASP.NET再从线程池取出线程继续处理请求,当请求处理完成时,回收线程到线程池。注意,在同步处理,同一个线程会一直在这个请求的生命周期,在异步处理不同的线程可能会处理同一个请求。


现在如果接收到3个请求,服务器可以轻松应付。因为每当请求等待异步工作时,它都会被释放到线程池中区,他们可以处理新的以及现有的请求。异步允许使用小数量县城处理更多的请求。因此ASP.NET异步代码的重要优点是可拓展性。


为什么不增加线程池的大小?


然而我们我为什么不直接去增加线程池的大小呢:答案有两个,相对于同步,异步代码有更好的拓展性和性能。


与同步编程相比异步编程能更好的拓展,因为它使用较少的内存。在现代操作系统上每个线程池有1M的栈空间,加上操作系统的unpageable内核栈。如果你在你的服务器使用大量的线程时,你会发现这些栈空间或许已经不足。相比之下,异步操作看起来占用的内存更少,因此具有异步操作的请求处理会拥有更多的吞吐量。自然,你就可以使用更多占用内存的东西(缓存等)。


异步编程可以更快,因为线程池的注入速率更快。在撰写本文时(14年),速度是每隔两秒钟一个线程。这种线程的注入限制是好事,它避免了构造和析构线程所消耗的时间。然而当瞬时高并发请求时候,同步编程往往很容易陷入线程池用尽,等待。异步编程则不会存在这个问题,所以说异步更能抗压。


不过要铭记一点,异步代码不能替代线程池。不是异步编程或线程池,是异步编程和线程池。异步编程允许您最大化的利用线程池。他会将现有的线程池中数量控制在11.


使用线程处理异步工作如何?


这个问题总是被问到。它的言下之意是必须有一些线程阻塞在I/O调用外部资源的地方。因此,异步线程释放掉当前请求的线程,但是会有另一个线程接盘。这正确么?答案是否定的。


要理解异步请求的伸缩性,我们先跟随一个异步IO的小例子。比如说一个请求需要写入一个文件,请求的线程调用一个异步写文件的方法。异步写文件的方法是基于BCL去实现的,并且使用IO完成端口。(注解一:什么是IO完成端口 IOCP)所以异步写文件调用被传递到操作系统,操作系统随后使用驱动的堆栈传递请求要写入的数据信息.


这是一件有趣的事:如果驱动不能立即处理IRP(IO request Package),它必须以异步方式处理。所以驱动告诉硬盘开始写,并且返回给OS一个"pendding"响应。操作系统将Pendding状态传递给BCL。BCL返回这个被完成的任务给请求处理的代码,请求处理等待这个任务,并且释放这个线程。


想一想当前的系统状态,多种IO结构已经被分类(例如task 和 IRP),他们都处于pending/imcomplete状态,然而没有线程处于阻塞状态等待写操作的完成。无论是asp.net BCL 还是操作系统又或者驱动,都没有一个线程去专门处理异步操作。


当硬盘写入完成时,他会通知驱动程序中断,驱动通知操作系统这个IRP已经完成,操作系统通过IO完成端口再通知BCL。线程池中的一个线程响应Task完成的消息,返回到异步写的方法反过来去恢复请求的代码。在这个过程中只有很少的线程被短时间借用,但是并没有任何线程在写入的时候被阻塞。


这是一个大大简化的例子,但是它缺横跨几个关键的点:没有一个线程必须异步工作,没有任何CPU时间有必要真的推送字节。想想在设备驱动的世界里,设备驱动程序怎么样去既处理同步又处理异步。同步并不是一个选项,很多开发人员都认为IO操作原生的API是同步操作,这是落后的思想,事实上,它天生是一部的,所谓的同步API也是通过异步去实现的。


先前为什么没有使用异步处理请求


如果异步请求处理是如此的美好,为什么之前没有人广泛使用?异步编程非常好,ASP.NET从很早就支持了。在asp.net 2.0中,就已经介绍了异步的web页面和MVC异步控制器。


然而,之前的异步代码一直很难编写和维护,很多公司采用更容易使用的同步代码和增加更大更多更贵的服务器去增加吞吐。但是现在,发生了逆转,在asp.net44.5,使用async/await非常容易就可以进行异步编程。随着大型系统迁移到伸缩性的云上,越来越多人人开始拥抱asp.net异步编程。


异步编程也不是银弹


虽然异步编程很棒,但它也不能帮你处理所有的问题。有几个关于ASP.NET async/await的误解。


一些开发人员学习async/await,他们认为服务端编程是依赖于客户端的例如浏览器。然而async/await只是依赖于asp.net runtime。HTTP协议保持不变,一个请求就有一个响应。如果你需要在异步之前使用signalR 或者AJAX 或者UpdatePanel ,那么你仍然需要他们。


使用async/await异步请求处理能提高你的应用程序的吞吐。然后,这只是仅仅用于单一服务器,你可能还需要增加服务器进行拓展。如果你想要支持横向拓展的架构,你就必须要考虑请求的幂等,和可靠的队列。Async/Await也能对你有所帮助。他们能使你重复的利用你的服务器资源,所以你不需要经常进行横向拓展。如果你确实需要横向拓展,那么你必须找到一个合适的分布式体系结构。


在ASP.NET中Async/Await都是关于IO的。他们真正擅长的是处理读取文件、数据库和调用其它ResultAPI。他们并不适合与CPU相关的工作,你可以通过awaiting task.Run踢掉一些背景工作,但是这样做并没有意义。事实上这会破会程序员的可拓展性,它会干扰ASP.NET线程池的直观判断。如果你有CPU的工作要在ASP.NET上进行处理。你最好在请求的线程上执行它。作为一个一般规则。在ASP,BET中不要排队到线程池。


最好,考虑一下你的系统作为一个整体的可伸缩性。在十年前,一个常见的结构是一个SQL server数据库对应一个asp.net web服务器。在这种体系结构中,通常数据库服务器是可拓展伸缩的,而不是web服务器。你的数据服务异步调用通常并没有什么卵用,你当然可以可以使用他们拓展web服务器,但是数据库服务会作为一个整体伸缩拓展。


Rick Anderson 已经发表一个关于Should My Database Calls Be Asynchronous的文章(https://blogs.msdn.microsoft.com/rickandy/2009/11/14/should-my-database-calls-be-asynchronous/),有这两个参数:1 异步代码很难(因此和开发人员的时间相比,只能购买较大的服务器) 2.web服务器伸缩并没有的多大意义,如果你的数据库后端是瓶颈。但是随着时间的推移,之前的观点已经站不住脚,1.异步编程已经非常容易 2.对于网站后端直接使用云计算,使用应运而生的azure sql数据库,nosql和其他api都可以超过单个sql服务器,所以最终还是将系统的瓶颈推回到web服务器端。这种情况下async/await会给asp.net应用程序的拓展带来福音。


在开始之前


首先你要制动啊async/await只有4.5+版本才支持。Nuget包叫做 Microsoft.Bcl.Async,它使async/await 在4.0中使用,如果不是用它,async/await将无法正常工作!。因为ASP.NET本身不能改变它的管理异步请求的方式,为了更好的已使用async/await。Nuget包虽然包含所有编译器需要的类型,但是不会修补你的asp.net运行时,所以你需要4.5版本以上。


接下来,要知道,ASP.NET 4.5 在服务器上引入了“quirks 模式”。如果您创建一个新的 ASP.NET 4.5 项目,则不必担心。但是,如果要将现有的项目升级到 ASP.NET 4.5,所有 quirk 都将被打开。我建议您​​通过编辑 web.config 并将 httpRuntime.targetFramework 设置为 4.5 把它们全部关闭。如果使用此设置的应用程序失败(并且您不想花时间去修复它),至少您可以通过为 aspnet:UseTaskFriendlySynchronizationContext 的 appSetting 键添加值“true”来获取 async/await 工作。如果您将 httpRuntime.targetFramework 设置为 4.5,则 appSetting 键不必要。Web 开发团队已在https://blogs.msdn.microsoft.com/webdev/2012/11/19/all-about-httpruntime-targetframework/发表一篇关于这一新的“quirks 模式”的详细信息的博客。提示: 如果您看到出现奇怪的行为或例外情况,并且您的调用堆栈包括 LegacyAspNetSynchronizationContext,那么您的应用程序正在这个“quirks 模式”下运行。LegacyAspNetSynchronizationContext 与异步不兼容;您在 ASP.NET 4.5 上需要常规的 AspNetSynchronizationContext。


在 ASP.NET 4.5 中,所有的 ASP.NET 设置都针对异步请求设置了很好的默认值,但也有几个其他设置您可能要更改。首先是 IIS 设置:考虑将 IIS/HTTP.sys 的队列限制(应用程序池|高级设置|队列长度)从默认的 1,000 提高到 5,000。另一个是 .NET 运行时设置:ServicePointManager.DefaultConnectionLimit,它的默认值是内核数量的 12 倍。DefaultConnectionLimit 限制到同一主机名的传出连接数。


关于中止请求的提示


当 ASP.NET 同步处理一个请求时,它有一个非常简单的机制可以中止请求(例如,如果请求超出其超时值):它会中止该请求的工作线程。这是有道理的,因为在同步领域,每个请求从开始到结束都使用同一个工作线程。中止线程对于 AppDomain 的长期稳定性而言尚不完美,因此默认情况下 ASP.NET 将定期回收您的应用程序,以保持干净。


对于异步请求,如果要中止请求,ASP.NET 并不会中止工作线程。相反,它会取消使用 CancellationToken 的请求。异步请求处理程序应该接受并遵守取消标记。大多数较新的框架(包括 Web API、MVC 和 SignalR)将构建并直接向您传递 CancellationToken;您需要做的就是把它声明为一个参数。您也可以直接访问 ASP.NET 标记;例如,HttpRequest.TimedOutToken 是当请求超时时取消的一个 CancellationToken。


随着应用程序迁移到云,中止请求就显得更为重要。基于云的应用程序也越来越依赖于可能占用任意时间量的外部服务。例如,一种标准模式是使用指数回退来重试外部请求;如果您的应用程序依赖于类似这样的多种服务,对您的请求处理在整体上应用一个超时上限不失为一个好方法。


Async 支持的现状


针对 async 的兼容性问题,已对许多库进行了更新。在版本 6 中已将 async 支持添加到实体框架(在 EntityFramework NuGet 程序包中)。不过,当以异步方式运行时,您必须要小心操作以避免延迟加载,因为延迟加载总是以同步方式执行。HttpClient(在 Microsoft.Net.Http NuGet 程序包中)是采用 async 理念设计而成的现代 HTTP 客户端,是调用外部 REST API 的理想选择;是 HttpWebRequest 和 WebClient 的现代版替代品。在 2.1 版本中,Microsoft Azure 存储客户端库(在 WindowsAzure.Storage NuGet 程序包中)添加了异步支持。


较新的框架(如 Web API 和 SignalR)对 async 和 await 提供全面的支持。个别 Web API 已围绕 async 支持建立起整个管道:不仅有异步控制器,还有异步筛选器和处理程序。Web API 和 SignalR 有一个很平凡的异步故事:您可以“放手去做”然后“就会成功”。


这给我们带来了一个令人伤感的故事:如今,ASP.NET MVC 只是部分支持 async 和 await。有基本的支持——异步控制器的操作和取消工作正常。ASP.NET 网站上有关于如何使用 ASP.NET MVC 中的异步控制器操作的精彩教程 (bit.ly/1m1LXTx);这对于 MVC 上的 async 入门是绝佳的资源。不幸的是,ASP.NET MVC (目前)不支持异步筛选器 (bit.ly/1oAyHLc) 和异步子操作 (bit.ly/1px47RG)。


ASP.NET Web 窗体是一个较旧的框架,但它也充分支持 async 和 await。并且,ASP.NET 网站上有关异步 Web 窗体的教程也是入门的绝佳资源 (bit.ly/Ydho7W)。有了 Web 窗体,异步支持可以选择加入。您必须先将 Page.Async 设置为 true,然后您可以使用 PageAsyncTask 通过该页面注册异步工作(或者,您可以使用 async void 事件处理程序)。PageAsyncTask 也支持取消。


如果您有一个自定义 HTTP 处理程序或 HTTP 模块,那么 ASP.NET 现在也可以支持它们的异步版本。HTTP 处理程序是通过 HttpTaskAsyncHandler (bit.ly/1nWpWFj) 进行支持的,HTTP 模块是通过 EventHandlerTaskAsyncHelper (bit.ly/1m1Sn4O) 进行支持的。


截至发稿时,ASP.NET 团队正在开发名为 ASP.NET vNext 的一个新项目。在 vNext 中,默认情况下整个管道是异步的。目前,该计划将 MVC 和 Web API 合并到能够全面支持 async/await(包括异步筛选器和异步视图组件)的单一框架中。其他异步就绪框架(如 SignalR)会在 vNext 中找到一个自然的家。当然,未来是 async 的天下。


尊重安全网


ASP.NET 4.5 中引入了几个新的“安全网”,帮助您捕捉应用程序中的异步问题。这些是默认情况下存在的,应当保留。


当同步处理程序试图执行异步工作时,您的 InvalidOperationException 会收到这样的消息,“此时不能开始异步操作”。有两个主要原因导致出现此异常。第一,Web 窗体页有异步事件处理程序,但忽略了将 Page.Async 设置为 true。第二,同步代码调用 async void 方法。这是也是避免 async void 的另一个原因。


另一个安全网适用于异步处理程序:当异步处理程序完成请求,但 ASP.NET 检测到异步工作尚未完成时,您的 InvalidOperationException 会收到这样的消息,“异步模块或处理程序已完成,但异步操作仍然处于挂起状态”。这通常是由于异步代码调用 async void 方法而导致的,但也可能是因为不当使用基于事件的异步模式 (EAP) 组件 (bit.ly/19VdUWu)。


您还可以使用一个选项来关闭这两个安全网:HttpContext.AllowAsyncDuringSyncStages(也可以在 web.config 中对它进行设置)。Internet 上的一些页面建议您在看到这些异常时进行这样的设置。我完全不同意。说真的,我不知道这怎么可行。禁用安全网是一个可怕的想法。我能够想到的唯一可能的原因是,您的代码可能已经进行了一些非常先进的异步处理(远超我曾经尝试过的范围),您是一个多线程处理的天才。所以,如果您已经阅读了整篇文章,边打着呵欠边想,“拜托,我可不是菜鸟”,那么我想你可以考虑禁用安全网。而对于我们中的其他人,这是一个非常危险的选项,除非您完全知晓后果,否则不应进行此设置。


开始使用


终于到最后了!准备好开始使用 async 和 await 了吗?我很欣赏您的耐心。


首先,查看本文的“异步代码不是灵丹妙药”一节以确保 async/await 对您的体系结构是有益的。接下来,将您的应用程序更新到 ASP.NET 4.5 并关闭 quirks 模式(此时若只为确保不发生中断,运行它也可以)。这时,您便可以开始真正的同步/等待工作了。


从“叶”开始。想想您的请求如何进行处理并标识任何基于 I/O 的操作,特别是基于网络的操作。常见的示例是数据库查询和命令,以及对其他 Web 服务和 API 的调用。选择一个来开始,并做一些调查来查找使用 async/await 执行该操作的最佳选择。.NET Framework 4.5 中有许多内置 BCL 类型目前都已异步就绪;例如,SmtpClient 具有 SendMailAsync 方法。某些类型可以提供异步就绪更换;例如,HttpWebRequest 和 Web 客户端可以用 HttpClient 来更换。如需要,请升级您的库版本;例如,EF6 中的实体框架具有异步兼容方法。


但是,要避免库中的“假异步”。假异步是这样一种现象:一个组件中具有一个异步就绪 API,而它只是通过将同步 API 封装在线程池线程内来实现的。这对于实现 ASP.NET 上的可扩展性适得其反。假异步的一个典型示例就是 Newtonsoft JSON.NET,一个其他方面很出色的库。最好不调用(假)异步版本来执行 JSON 的序列化;只需换做调用同步版本即可。假异步的一个棘手示例就是 BCL 文件流。当打开一个文件流时,必须以显式方式打开以便于异步访问;否则,它会使用假异步,同步阻止文件读取和写入操作中的线程池线程。


选择一个“叶”之后,就可以开始使用代码中调用该 API 的方法,使之成为通过等待调用异步就绪 API 的异步方法。如果您调用的 API 支持 CancellationToken,您的方法应该采用 CancellationToken 并将其传递给 API 方法。


只要将一个方法标记为异步,就应该更改其返回类型:void 变为“Task”,非 void 类型 T 变为“Task”。您会发现,所有该方法的调用者都需要变为异步,以使它们能够等待任务,等等。此外,将 Async 附加到您方法的名称中,以遵循基于任务的异步模式约定 (bit.ly/1uBKGKR)。


允许 async/await 模式将您的调用堆栈向“Trunk”进行扩展。在 Trunk 中,您的代码将与 ASP.NET 框架(MVC、Web 窗体,Web API)相连接。阅读本文前面所述的“异步支持的现状”一节中的相关教程,将您的异步代码与框架进行集成。


顺便找出任何本地线程的状态。由于异步请求可能会更改线程,所以本地线程状态(如 ThreadStaticAttribute、ThreadLocal、线程数据插槽和 CallContext.GetData/SetData)将不可用。如果可能的话,请使用 HttpContext.Items 进行更换;或者您可以将不可变的数据存储在 CallContext.LogicalGetData/LogicalSetData 中。


以下是我发现的有用技巧:您可以(暂时)复制您的代码来创建一个垂直分区。利用这种技术,您不用将同步方法更改为异步;可以复制整个同步方法,然后将副本改为异步。然后,您可以让大多数应用程序保持使用同步方法,只创建异步的一个小垂直片即可。如果您想把异步作为概念证明来进行探索或只针对应用程序的一部分执行负载测试来体验怎样扩展您的系统,这会是一个很棒的主意。您可以具有一个完全异步的请求(或页面),而应用程序的其余部分保持为同步。当然,您不希望对每一个方法都保留副本;最终,所有的 I/O 绑定代码都将是异步的,可以删除同步副本。


总结


我希望本文能够帮助您了解 ASP.NET 上的异步请求的基础概念。使用 async 和 await,可以使编写能够最大限度地利用其服务器资源的 Web 应用程序、服务和 API 变得比以往任何时候都更容易。Async 真是太棒了

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台